mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge branch 'DSpace:main' into german-translations
This commit is contained in:
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -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
|
||||||
|
@@ -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.
|
||||||
|
|
||||||
|
@@ -63,7 +63,8 @@
|
|||||||
"bundleName": "dspace-theme"
|
"bundleName": "dspace-theme"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": [],
|
||||||
|
"baseHref": "/"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"development": {
|
"development": {
|
||||||
|
@@ -25,7 +25,7 @@ services:
|
|||||||
### OVERRIDE default 'entrypoint' in 'docker-compose-rest.yml' ####
|
### OVERRIDE default 'entrypoint' in 'docker-compose-rest.yml' ####
|
||||||
# Ensure that the database is ready BEFORE starting tomcat
|
# Ensure that the database is ready BEFORE starting tomcat
|
||||||
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
|
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
|
||||||
# 2. Then, run database migration to init database tables
|
# 2. Then, run database migration to init database tables (including any out-of-order ignored migrations, if any)
|
||||||
# 3. (Custom for Entities) enable Entity-specific collection submission mappings in item-submission.xml
|
# 3. (Custom for Entities) enable Entity-specific collection submission mappings in item-submission.xml
|
||||||
# This 'sed' command inserts the sample configurations specific to the Entities data set, see:
|
# This 'sed' command inserts the sample configurations specific to the Entities data set, see:
|
||||||
# https://github.com/DSpace/DSpace/blob/main/dspace/config/item-submission.xml#L36-L49
|
# https://github.com/DSpace/DSpace/blob/main/dspace/config/item-submission.xml#L36-L49
|
||||||
@@ -35,7 +35,7 @@ services:
|
|||||||
- '-c'
|
- '-c'
|
||||||
- |
|
- |
|
||||||
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
||||||
/dspace/bin/dspace database migrate
|
/dspace/bin/dspace database migrate ignored
|
||||||
sed -i '/name-map collection-handle="default".*/a \\n <name-map collection-handle="123456789/3" submission-name="Publication"/> \
|
sed -i '/name-map collection-handle="default".*/a \\n <name-map collection-handle="123456789/3" submission-name="Publication"/> \
|
||||||
<name-map collection-handle="123456789/4" submission-name="Publication"/> \
|
<name-map collection-handle="123456789/4" submission-name="Publication"/> \
|
||||||
<name-map collection-handle="123456789/281" submission-name="Publication"/> \
|
<name-map collection-handle="123456789/281" submission-name="Publication"/> \
|
||||||
|
@@ -46,14 +46,14 @@ services:
|
|||||||
- solr_configs:/dspace/solr
|
- solr_configs:/dspace/solr
|
||||||
# Ensure that the database is ready BEFORE starting tomcat
|
# Ensure that the database is ready BEFORE starting tomcat
|
||||||
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
|
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
|
||||||
# 2. Then, run database migration to init database tables
|
# 2. Then, run database migration to init database tables (including any out-of-order ignored migrations, if any)
|
||||||
# 3. Finally, start Tomcat
|
# 3. Finally, start Tomcat
|
||||||
entrypoint:
|
entrypoint:
|
||||||
- /bin/bash
|
- /bin/bash
|
||||||
- '-c'
|
- '-c'
|
||||||
- |
|
- |
|
||||||
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
||||||
/dspace/bin/dspace database migrate
|
/dspace/bin/dspace database migrate ignored
|
||||||
catalina.sh run
|
catalina.sh run
|
||||||
# DSpace database container
|
# DSpace database container
|
||||||
# NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data
|
# NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data
|
||||||
|
@@ -18,8 +18,8 @@ import { ItemAdminSearchResultGridElementComponent } from './item-admin-search-r
|
|||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
||||||
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
|
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
|
||||||
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
||||||
import { AccessStatusDataService } from 'src/app/core/data/access-status-data.service';
|
import { AccessStatusDataService } from '../../../../../core/data/access-status-data.service';
|
||||||
import { AccessStatusObject } from 'src/app/shared/object-list/access-status-badge/access-status.model';
|
import { AccessStatusObject } from '../../../../../shared/object-list/access-status-badge/access-status.model';
|
||||||
|
|
||||||
describe('ItemAdminSearchResultGridElementComponent', () => {
|
describe('ItemAdminSearchResultGridElementComponent', () => {
|
||||||
let component: ItemAdminSearchResultGridElementComponent;
|
let component: ItemAdminSearchResultGridElementComponent;
|
||||||
|
@@ -107,6 +107,8 @@ export function getPageInternalServerErrorRoute() {
|
|||||||
return `/${INTERNAL_SERVER_ERROR}`;
|
return `/${INTERNAL_SERVER_ERROR}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ERROR_PAGE = 'error';
|
||||||
|
|
||||||
export const INFO_MODULE_PATH = 'info';
|
export const INFO_MODULE_PATH = 'info';
|
||||||
export function getInfoModulePath() {
|
export function getInfoModulePath() {
|
||||||
return `/${INFO_MODULE_PATH}`;
|
return `/${INFO_MODULE_PATH}`;
|
||||||
|
@@ -10,6 +10,7 @@ import {
|
|||||||
ACCESS_CONTROL_MODULE_PATH,
|
ACCESS_CONTROL_MODULE_PATH,
|
||||||
ADMIN_MODULE_PATH,
|
ADMIN_MODULE_PATH,
|
||||||
BITSTREAM_MODULE_PATH,
|
BITSTREAM_MODULE_PATH,
|
||||||
|
ERROR_PAGE,
|
||||||
FORBIDDEN_PATH,
|
FORBIDDEN_PATH,
|
||||||
FORGOT_PASSWORD_PATH,
|
FORGOT_PASSWORD_PATH,
|
||||||
HEALTH_PAGE_PATH,
|
HEALTH_PAGE_PATH,
|
||||||
@@ -38,11 +39,13 @@ import {
|
|||||||
} from './page-internal-server-error/themed-page-internal-server-error.component';
|
} from './page-internal-server-error/themed-page-internal-server-error.component';
|
||||||
import { ServerCheckGuard } from './core/server-check/server-check.guard';
|
import { ServerCheckGuard } from './core/server-check/server-check.guard';
|
||||||
import { MenuResolver } from './menu.resolver';
|
import { MenuResolver } from './menu.resolver';
|
||||||
|
import { ThemedPageErrorComponent } from './page-error/themed-page-error.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forRoot([
|
RouterModule.forRoot([
|
||||||
{ path: INTERNAL_SERVER_ERROR, component: ThemedPageInternalServerErrorComponent },
|
{ path: INTERNAL_SERVER_ERROR, component: ThemedPageInternalServerErrorComponent },
|
||||||
|
{ path: ERROR_PAGE , component: ThemedPageErrorComponent },
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
canActivate: [AuthBlockingGuard],
|
canActivate: [AuthBlockingGuard],
|
||||||
|
@@ -22,7 +22,7 @@ import {
|
|||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import { BehaviorSubject, Observable, of } from 'rxjs';
|
import { BehaviorSubject, Observable, of } from 'rxjs';
|
||||||
import { select, Store } from '@ngrx/store';
|
import { select, Store } from '@ngrx/store';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal, NgbModalConfig } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { Angulartics2GoogleAnalytics } from 'angulartics2';
|
import { Angulartics2GoogleAnalytics } from 'angulartics2';
|
||||||
|
|
||||||
@@ -49,6 +49,7 @@ import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service';
|
|||||||
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
|
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
|
||||||
import { getDefaultThemeConfig } from '../config/config.util';
|
import { getDefaultThemeConfig } from '../config/config.util';
|
||||||
import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface';
|
import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface';
|
||||||
|
import { ModalBeforeDismiss } from './shared/interfaces/modal-before-dismiss.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-app',
|
selector: 'ds-app',
|
||||||
@@ -106,6 +107,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
private localeService: LocaleService,
|
private localeService: LocaleService,
|
||||||
private breadcrumbsService: BreadcrumbsService,
|
private breadcrumbsService: BreadcrumbsService,
|
||||||
private modalService: NgbModal,
|
private modalService: NgbModal,
|
||||||
|
private modalConfig: NgbModalConfig,
|
||||||
@Optional() private cookiesService: KlaroService,
|
@Optional() private cookiesService: KlaroService,
|
||||||
@Optional() private googleAnalyticsService: GoogleAnalyticsService,
|
@Optional() private googleAnalyticsService: GoogleAnalyticsService,
|
||||||
) {
|
) {
|
||||||
@@ -166,6 +168,16 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
/** Implement behavior for interface {@link ModalBeforeDismiss} */
|
||||||
|
this.modalConfig.beforeDismiss = async function () {
|
||||||
|
if (typeof this?.componentInstance?.beforeDismiss === 'function') {
|
||||||
|
return this.componentInstance.beforeDismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
// fall back to default behavior
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
this.isAuthBlocking$ = this.store.pipe(select(isAuthenticationBlocking)).pipe(
|
this.isAuthBlocking$ = this.store.pipe(select(isAuthenticationBlocking)).pipe(
|
||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
);
|
);
|
||||||
|
@@ -4,5 +4,6 @@ export enum AuthMethodType {
|
|||||||
Ldap = 'ldap',
|
Ldap = 'ldap',
|
||||||
Ip = 'ip',
|
Ip = 'ip',
|
||||||
X509 = 'x509',
|
X509 = 'x509',
|
||||||
Oidc = 'oidc'
|
Oidc = 'oidc',
|
||||||
|
Orcid = 'orcid'
|
||||||
}
|
}
|
||||||
|
@@ -34,6 +34,11 @@ export class AuthMethod {
|
|||||||
this.location = location;
|
this.location = location;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'orcid': {
|
||||||
|
this.authMethodType = AuthMethodType.Orcid;
|
||||||
|
this.location = location;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
break;
|
break;
|
||||||
|
@@ -38,7 +38,7 @@ import { SubmissionSectionModel } from './config/models/config-submission-sectio
|
|||||||
import { SubmissionUploadsModel } from './config/models/config-submission-uploads.model';
|
import { SubmissionUploadsModel } from './config/models/config-submission-uploads.model';
|
||||||
import { SubmissionFormsConfigService } from './config/submission-forms-config.service';
|
import { SubmissionFormsConfigService } from './config/submission-forms-config.service';
|
||||||
import { coreEffects } from './core.effects';
|
import { coreEffects } from './core.effects';
|
||||||
import { coreReducers} from './core.reducers';
|
import { coreReducers } from './core.reducers';
|
||||||
import { BitstreamFormatDataService } from './data/bitstream-format-data.service';
|
import { BitstreamFormatDataService } from './data/bitstream-format-data.service';
|
||||||
import { CollectionDataService } from './data/collection-data.service';
|
import { CollectionDataService } from './data/collection-data.service';
|
||||||
import { CommunityDataService } from './data/community-data.service';
|
import { CommunityDataService } from './data/community-data.service';
|
||||||
@@ -132,11 +132,15 @@ import { Feature } from './shared/feature.model';
|
|||||||
import { Authorization } from './shared/authorization.model';
|
import { Authorization } from './shared/authorization.model';
|
||||||
import { FeatureDataService } from './data/feature-authorization/feature-data.service';
|
import { FeatureDataService } from './data/feature-authorization/feature-data.service';
|
||||||
import { AuthorizationDataService } from './data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from './data/feature-authorization/authorization-data.service';
|
||||||
import { SiteAdministratorGuard } from './data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
import {
|
||||||
|
SiteAdministratorGuard
|
||||||
|
} from './data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||||
import { Registration } from './shared/registration.model';
|
import { Registration } from './shared/registration.model';
|
||||||
import { MetadataSchemaDataService } from './data/metadata-schema-data.service';
|
import { MetadataSchemaDataService } from './data/metadata-schema-data.service';
|
||||||
import { MetadataFieldDataService } from './data/metadata-field-data.service';
|
import { MetadataFieldDataService } from './data/metadata-field-data.service';
|
||||||
import { DsDynamicTypeBindRelationService } from '../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service';
|
import {
|
||||||
|
DsDynamicTypeBindRelationService
|
||||||
|
} from '../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service';
|
||||||
import { TokenResponseParsingService } from './auth/token-response-parsing.service';
|
import { TokenResponseParsingService } from './auth/token-response-parsing.service';
|
||||||
import { SubmissionCcLicenseDataService } from './submission/submission-cc-license-data.service';
|
import { SubmissionCcLicenseDataService } from './submission/submission-cc-license-data.service';
|
||||||
import { SubmissionCcLicence } from './submission/models/submission-cc-license.model';
|
import { SubmissionCcLicence } from './submission/models/submission-cc-license.model';
|
||||||
@@ -169,6 +173,11 @@ import { LinkHeadService } from './services/link-head.service';
|
|||||||
import { ResearcherProfileService } from './profile/researcher-profile.service';
|
import { 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
|
||||||
@@ -296,7 +305,10 @@ const PROVIDERS = [
|
|||||||
GroupDataService,
|
GroupDataService,
|
||||||
FeedbackDataService,
|
FeedbackDataService,
|
||||||
ResearcherProfileService,
|
ResearcherProfileService,
|
||||||
ProfileClaimService
|
ProfileClaimService,
|
||||||
|
OrcidAuthService,
|
||||||
|
OrcidQueueService,
|
||||||
|
OrcidHistoryDataService,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -358,7 +370,10 @@ export const models =
|
|||||||
SearchConfig,
|
SearchConfig,
|
||||||
SubmissionAccessesModel,
|
SubmissionAccessesModel,
|
||||||
AccessStatusObject,
|
AccessStatusObject,
|
||||||
ResearcherProfile
|
ResearcherProfile,
|
||||||
|
OrcidQueue,
|
||||||
|
OrcidHistory,
|
||||||
|
AccessStatusObject
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@@ -29,4 +29,5 @@ export enum FeatureID {
|
|||||||
CanViewUsageStatistics = 'canViewUsageStatistics',
|
CanViewUsageStatistics = 'canViewUsageStatistics',
|
||||||
CanSendFeedback = 'canSendFeedback',
|
CanSendFeedback = 'canSendFeedback',
|
||||||
CanClaimItem = 'canClaimItem',
|
CanClaimItem = 'canClaimItem',
|
||||||
|
CanSynchronizeWithORCID = 'canSynchronizeWithORCID'
|
||||||
}
|
}
|
||||||
|
89
src/app/core/orcid/model/orcid-history.model.ts
Normal file
89
src/app/core/orcid/model/orcid-history.model.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { autoserialize, deserialize } from 'cerialize';
|
||||||
|
import { typedObject } from '../../cache/builders/build-decorators';
|
||||||
|
import { HALLink } from '../../shared/hal-link.model';
|
||||||
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
|
import { excludeFromEquals } from '../../utilities/equals.decorators';
|
||||||
|
import { ORCID_HISTORY } from './orcid-history.resource-type';
|
||||||
|
import { CacheableObject } from '../../cache/cacheable-object.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class the represents a Orcid History.
|
||||||
|
*/
|
||||||
|
@typedObject
|
||||||
|
export class OrcidHistory extends CacheableObject {
|
||||||
|
|
||||||
|
static type = ORCID_HISTORY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The object type
|
||||||
|
*/
|
||||||
|
@excludeFromEquals
|
||||||
|
@autoserialize
|
||||||
|
type: ResourceType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier of this Orcid History record
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the related entity
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
entityName: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier of the profileItem of this Orcid History record.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
profileItemId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier of the entity related to this Orcid History record.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
entityId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the entity related to this Orcid History record.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
entityType: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The response status coming from ORCID api.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
status: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The putCode assigned by ORCID to the entity.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
putCode: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The last send attempt timestamp.
|
||||||
|
*/
|
||||||
|
lastAttempt: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The success send attempt timestamp.
|
||||||
|
*/
|
||||||
|
successAttempt: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The response coming from ORCID.
|
||||||
|
*/
|
||||||
|
responseMessage: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link HALLink}s for this Orcid History record
|
||||||
|
*/
|
||||||
|
@deserialize
|
||||||
|
_links: {
|
||||||
|
self: HALLink,
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
9
src/app/core/orcid/model/orcid-history.resource-type.ts
Normal file
9
src/app/core/orcid/model/orcid-history.resource-type.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource type for OrcidHistory
|
||||||
|
*
|
||||||
|
* Needs to be in a separate file to prevent circular
|
||||||
|
* dependencies in webpack.
|
||||||
|
*/
|
||||||
|
export const ORCID_HISTORY = new ResourceType('orcidhistory');
|
68
src/app/core/orcid/model/orcid-queue.model.ts
Normal file
68
src/app/core/orcid/model/orcid-queue.model.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { autoserialize, deserialize } from 'cerialize';
|
||||||
|
import { typedObject } from '../../cache/builders/build-decorators';
|
||||||
|
import { HALLink } from '../../shared/hal-link.model';
|
||||||
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
|
import { excludeFromEquals } from '../../utilities/equals.decorators';
|
||||||
|
import { ORCID_QUEUE } from './orcid-queue.resource-type';
|
||||||
|
import { CacheableObject } from '../../cache/cacheable-object.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class the represents a Orcid Queue.
|
||||||
|
*/
|
||||||
|
@typedObject
|
||||||
|
export class OrcidQueue extends CacheableObject {
|
||||||
|
|
||||||
|
static type = ORCID_QUEUE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The object type
|
||||||
|
*/
|
||||||
|
@excludeFromEquals
|
||||||
|
@autoserialize
|
||||||
|
type: ResourceType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier of this Orcid Queue record
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The record description.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier of the profileItem of this Orcid Queue record.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
profileItemId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier of the entity related to this Orcid Queue record.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
entityId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of this Orcid Queue record.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
recordType: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The operation related to this Orcid Queue record.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
operation: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link HALLink}s for this Orcid Queue record
|
||||||
|
*/
|
||||||
|
@deserialize
|
||||||
|
_links: {
|
||||||
|
self: HALLink,
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
9
src/app/core/orcid/model/orcid-queue.resource-type.ts
Normal file
9
src/app/core/orcid/model/orcid-queue.resource-type.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource type for OrcidQueue
|
||||||
|
*
|
||||||
|
* Needs to be in a separate file to prevent circular
|
||||||
|
* dependencies in webpack.
|
||||||
|
*/
|
||||||
|
export const ORCID_QUEUE = new ResourceType('orcidqueue');
|
329
src/app/core/orcid/orcid-auth.service.spec.ts
Normal file
329
src/app/core/orcid/orcid-auth.service.spec.ts
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { RouterMock } from '../../shared/mocks/router.mock';
|
||||||
|
import { ResearcherProfile } from '../profile/model/researcher-profile.model';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
import { AddOperation, RemoveOperation } from 'fast-json-patch';
|
||||||
|
import { ConfigurationProperty } from '../shared/configuration-property.model';
|
||||||
|
import { ConfigurationDataService } from '../data/configuration-data.service';
|
||||||
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
|
import { NativeWindowRefMock } from '../../shared/mocks/mock-native-window-ref';
|
||||||
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
|
import { OrcidAuthService } from './orcid-auth.service';
|
||||||
|
import { ResearcherProfileService } from '../profile/researcher-profile.service';
|
||||||
|
|
||||||
|
describe('OrcidAuthService', () => {
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
let service: OrcidAuthService;
|
||||||
|
let serviceAsAny: any;
|
||||||
|
|
||||||
|
let researcherProfileService: jasmine.SpyObj<ResearcherProfileService>;
|
||||||
|
let configurationDataService: ConfigurationDataService;
|
||||||
|
let nativeWindowService: NativeWindowRefMock;
|
||||||
|
let routerStub: any;
|
||||||
|
|
||||||
|
const researcherProfileId = 'beef9946-rt56-479e-8f11-b90cbe9f7241';
|
||||||
|
const itemId = 'beef9946-rt56-479e-8f11-b90cbe9f7241';
|
||||||
|
|
||||||
|
const researcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), {
|
||||||
|
id: researcherProfileId,
|
||||||
|
visible: false,
|
||||||
|
type: 'profile',
|
||||||
|
_links: {
|
||||||
|
item: {
|
||||||
|
href: `https://rest.api/rest/api/profiles/${researcherProfileId}/item`
|
||||||
|
},
|
||||||
|
self: {
|
||||||
|
href: `https://rest.api/rest/api/profiles/${researcherProfileId}`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const researcherProfilePatched: ResearcherProfile = Object.assign(new ResearcherProfile(), {
|
||||||
|
id: researcherProfileId,
|
||||||
|
visible: true,
|
||||||
|
type: 'profile',
|
||||||
|
_links: {
|
||||||
|
item: {
|
||||||
|
href: `https://rest.api/rest/api/profiles/${researcherProfileId}/item`
|
||||||
|
},
|
||||||
|
self: {
|
||||||
|
href: `https://rest.api/rest/api/profiles/${researcherProfileId}`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockItemUnlinkedToOrcid: Item = Object.assign(new Item(), {
|
||||||
|
id: 'mockItemUnlinkedToOrcid',
|
||||||
|
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [{
|
||||||
|
value: 'test person'
|
||||||
|
}],
|
||||||
|
'dspace.entity.type': [{
|
||||||
|
'value': 'Person'
|
||||||
|
}],
|
||||||
|
'dspace.object.owner': [{
|
||||||
|
'value': 'test person',
|
||||||
|
'language': null,
|
||||||
|
'authority': 'researcher-profile-id',
|
||||||
|
'confidence': 600,
|
||||||
|
'place': 0
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockItemLinkedToOrcid: Item = Object.assign(new Item(), {
|
||||||
|
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [{
|
||||||
|
value: 'test person'
|
||||||
|
}],
|
||||||
|
'dspace.entity.type': [{
|
||||||
|
'value': 'Person'
|
||||||
|
}],
|
||||||
|
'dspace.object.owner': [{
|
||||||
|
'value': 'test person',
|
||||||
|
'language': null,
|
||||||
|
'authority': 'researcher-profile-id',
|
||||||
|
'confidence': 600,
|
||||||
|
'place': 0
|
||||||
|
}],
|
||||||
|
'dspace.orcid.authenticated': [{
|
||||||
|
'value': '2022-06-10T15:15:12.952872',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 0
|
||||||
|
}],
|
||||||
|
'dspace.orcid.scope': [{
|
||||||
|
'value': '/authenticate',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 0
|
||||||
|
}, {
|
||||||
|
'value': '/read-limited',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 1
|
||||||
|
}, {
|
||||||
|
'value': '/activities/update',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 2
|
||||||
|
}, {
|
||||||
|
'value': '/person/update',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 3
|
||||||
|
}],
|
||||||
|
'person.identifier.orcid': [{
|
||||||
|
'value': 'orcid-id',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 0
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const disconnectionAllowAdmin = {
|
||||||
|
uuid: 'orcid.disconnection.allowed-users',
|
||||||
|
name: 'orcid.disconnection.allowed-users',
|
||||||
|
values: ['only_admin']
|
||||||
|
} as ConfigurationProperty;
|
||||||
|
|
||||||
|
const disconnectionAllowAdminOwner = {
|
||||||
|
uuid: 'orcid.disconnection.allowed-users',
|
||||||
|
name: 'orcid.disconnection.allowed-users',
|
||||||
|
values: ['admin_and_owner']
|
||||||
|
} as ConfigurationProperty;
|
||||||
|
|
||||||
|
const authorizeUrl = {
|
||||||
|
uuid: 'orcid.authorize-url',
|
||||||
|
name: 'orcid.authorize-url',
|
||||||
|
values: ['orcid.authorize-url']
|
||||||
|
} as ConfigurationProperty;
|
||||||
|
const appClientId = {
|
||||||
|
uuid: 'orcid.application-client-id',
|
||||||
|
name: 'orcid.application-client-id',
|
||||||
|
values: ['orcid.application-client-id']
|
||||||
|
} as ConfigurationProperty;
|
||||||
|
const orcidScope = {
|
||||||
|
uuid: 'orcid.scope',
|
||||||
|
name: 'orcid.scope',
|
||||||
|
values: ['/authenticate', '/read-limited']
|
||||||
|
} as ConfigurationProperty;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
routerStub = new RouterMock();
|
||||||
|
researcherProfileService = jasmine.createSpyObj('ResearcherProfileService', {
|
||||||
|
findById: jasmine.createSpy('findById'),
|
||||||
|
updateByOrcidOperations: jasmine.createSpy('updateByOrcidOperations')
|
||||||
|
});
|
||||||
|
configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
||||||
|
findByPropertyName: jasmine.createSpy('findByPropertyName')
|
||||||
|
});
|
||||||
|
nativeWindowService = new NativeWindowRefMock();
|
||||||
|
|
||||||
|
service = new OrcidAuthService(
|
||||||
|
nativeWindowService,
|
||||||
|
configurationDataService,
|
||||||
|
researcherProfileService,
|
||||||
|
routerStub);
|
||||||
|
|
||||||
|
serviceAsAny = service;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('isLinkedToOrcid', () => {
|
||||||
|
it('should return true when item has metadata', () => {
|
||||||
|
const result = service.isLinkedToOrcid(mockItemLinkedToOrcid);
|
||||||
|
expect(result).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when item has no metadata', () => {
|
||||||
|
const result = service.isLinkedToOrcid(mockItemUnlinkedToOrcid);
|
||||||
|
expect(result).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onlyAdminCanDisconnectProfileFromOrcid', () => {
|
||||||
|
it('should return true when property is only_admin', () => {
|
||||||
|
spyOn((service as any), 'getOrcidDisconnectionAllowedUsersConfiguration').and.returnValue(createSuccessfulRemoteDataObject$(disconnectionAllowAdmin));
|
||||||
|
const result = service.onlyAdminCanDisconnectProfileFromOrcid();
|
||||||
|
const expected = cold('(a|)', {
|
||||||
|
a: true
|
||||||
|
});
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false on faild', () => {
|
||||||
|
spyOn((service as any), 'getOrcidDisconnectionAllowedUsersConfiguration').and.returnValue(createFailedRemoteDataObject$());
|
||||||
|
const result = service.onlyAdminCanDisconnectProfileFromOrcid();
|
||||||
|
const expected = cold('(a|)', {
|
||||||
|
a: false
|
||||||
|
});
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ownerCanDisconnectProfileFromOrcid', () => {
|
||||||
|
it('should return true when property is admin_and_owner', () => {
|
||||||
|
spyOn((service as any), 'getOrcidDisconnectionAllowedUsersConfiguration').and.returnValue(createSuccessfulRemoteDataObject$(disconnectionAllowAdminOwner));
|
||||||
|
const result = service.ownerCanDisconnectProfileFromOrcid();
|
||||||
|
const expected = cold('(a|)', {
|
||||||
|
a: true
|
||||||
|
});
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false on faild', () => {
|
||||||
|
spyOn((service as any), 'getOrcidDisconnectionAllowedUsersConfiguration').and.returnValue(createFailedRemoteDataObject$());
|
||||||
|
const result = service.ownerCanDisconnectProfileFromOrcid();
|
||||||
|
const expected = cold('(a|)', {
|
||||||
|
a: false
|
||||||
|
});
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('linkOrcidByItem', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
researcherProfileService.updateByOrcidOperations.and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
|
||||||
|
researcherProfileService.findById.and.returnValue(createSuccessfulRemoteDataObject$(researcherProfile));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call updateByOrcidOperations method properly', () => {
|
||||||
|
const operations: AddOperation<string>[] = [{
|
||||||
|
path: '/orcid',
|
||||||
|
op: 'add',
|
||||||
|
value: 'test-code'
|
||||||
|
}];
|
||||||
|
|
||||||
|
scheduler.schedule(() => service.linkOrcidByItem(mockItemUnlinkedToOrcid, 'test-code').subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(researcherProfileService.updateByOrcidOperations).toHaveBeenCalledWith(researcherProfile, operations);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('unlinkOrcidByItem', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
researcherProfileService.updateByOrcidOperations.and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
|
||||||
|
researcherProfileService.findById.and.returnValue(createSuccessfulRemoteDataObject$(researcherProfile));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call updateByOrcidOperations method properly', () => {
|
||||||
|
const operations: RemoveOperation[] = [{
|
||||||
|
path: '/orcid',
|
||||||
|
op: 'remove'
|
||||||
|
}];
|
||||||
|
|
||||||
|
scheduler.schedule(() => service.unlinkOrcidByItem(mockItemLinkedToOrcid).subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(researcherProfileService.updateByOrcidOperations).toHaveBeenCalledWith(researcherProfile, operations);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getOrcidAuthorizeUrl', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
routerStub.setRoute('/entities/person/uuid/orcid');
|
||||||
|
(service as any).configurationService.findByPropertyName.and.returnValues(
|
||||||
|
createSuccessfulRemoteDataObject$(authorizeUrl),
|
||||||
|
createSuccessfulRemoteDataObject$(appClientId),
|
||||||
|
createSuccessfulRemoteDataObject$(orcidScope)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should build the url properly', () => {
|
||||||
|
const result = service.getOrcidAuthorizeUrl(mockItemUnlinkedToOrcid);
|
||||||
|
const redirectUri: string = new URLCombiner(nativeWindowService.nativeWindow.origin, encodeURIComponent(routerStub.url.split('?')[0])).toString();
|
||||||
|
const url = 'orcid.authorize-url?client_id=orcid.application-client-id&redirect_uri=' + redirectUri + '&response_type=code&scope=/authenticate /read-limited';
|
||||||
|
|
||||||
|
const expected = cold('(a|)', {
|
||||||
|
a: url
|
||||||
|
});
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getOrcidAuthorizationScopesByItem', () => {
|
||||||
|
it('should return list of scopes saved in the item', () => {
|
||||||
|
const orcidScopes = [
|
||||||
|
'/authenticate',
|
||||||
|
'/read-limited',
|
||||||
|
'/activities/update',
|
||||||
|
'/person/update'
|
||||||
|
];
|
||||||
|
const result = service.getOrcidAuthorizationScopesByItem(mockItemLinkedToOrcid);
|
||||||
|
expect(result).toEqual(orcidScopes);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getOrcidAuthorizationScopes', () => {
|
||||||
|
it('should return list of scopes by configuration', () => {
|
||||||
|
(service as any).configurationService.findByPropertyName.and.returnValue(
|
||||||
|
createSuccessfulRemoteDataObject$(orcidScope)
|
||||||
|
);
|
||||||
|
const orcidScopes = [
|
||||||
|
'/authenticate',
|
||||||
|
'/read-limited'
|
||||||
|
];
|
||||||
|
const expected = cold('(a|)', {
|
||||||
|
a: orcidScopes
|
||||||
|
});
|
||||||
|
const result = service.getOrcidAuthorizationScopes();
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
145
src/app/core/orcid/orcid-auth.service.ts
Normal file
145
src/app/core/orcid/orcid-auth.service.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
import { combineLatest, Observable } from 'rxjs';
|
||||||
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
|
import { AddOperation, RemoveOperation } from 'fast-json-patch';
|
||||||
|
|
||||||
|
import { ResearcherProfileService } from '../profile/researcher-profile.service';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../shared/operators';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { ConfigurationProperty } from '../shared/configuration-property.model';
|
||||||
|
import { ConfigurationDataService } from '../data/configuration-data.service';
|
||||||
|
import { ResearcherProfile } from '../profile/model/researcher-profile.model';
|
||||||
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
|
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OrcidAuthService {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
||||||
|
private configurationService: ConfigurationDataService,
|
||||||
|
private researcherProfileService: ResearcherProfileService,
|
||||||
|
private router: Router) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given item is linked to an ORCID profile.
|
||||||
|
*
|
||||||
|
* @param item the item to check
|
||||||
|
* @returns the check result
|
||||||
|
*/
|
||||||
|
public isLinkedToOrcid(item: Item): boolean {
|
||||||
|
return item.hasMetadata('dspace.orcid.authenticated');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if only the admin users can disconnect a researcher profile from ORCID.
|
||||||
|
*
|
||||||
|
* @returns the check result
|
||||||
|
*/
|
||||||
|
public onlyAdminCanDisconnectProfileFromOrcid(): Observable<boolean> {
|
||||||
|
return this.getOrcidDisconnectionAllowedUsersConfiguration().pipe(
|
||||||
|
map((propertyRD: RemoteData<ConfigurationProperty>) => {
|
||||||
|
return propertyRD.hasSucceeded && propertyRD.payload.values.map((value) => value.toLowerCase()).includes('only_admin');
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the profile's owner can disconnect that profile from ORCID.
|
||||||
|
*
|
||||||
|
* @returns the check result
|
||||||
|
*/
|
||||||
|
public ownerCanDisconnectProfileFromOrcid(): Observable<boolean> {
|
||||||
|
return this.getOrcidDisconnectionAllowedUsersConfiguration().pipe(
|
||||||
|
map((propertyRD: RemoteData<ConfigurationProperty>) => {
|
||||||
|
return propertyRD.hasSucceeded && propertyRD.payload.values.map( (value) => value.toLowerCase()).includes('admin_and_owner');
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a link operation to ORCID profile.
|
||||||
|
*
|
||||||
|
* @param person The person item related to the researcher profile
|
||||||
|
* @param code The auth-code received from orcid
|
||||||
|
*/
|
||||||
|
public linkOrcidByItem(person: Item, code: string): Observable<RemoteData<ResearcherProfile>> {
|
||||||
|
const operations: AddOperation<string>[] = [{
|
||||||
|
path: '/orcid',
|
||||||
|
op: 'add',
|
||||||
|
value: code
|
||||||
|
}];
|
||||||
|
|
||||||
|
return this.researcherProfileService.findById(person.firstMetadata('dspace.object.owner').authority).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
switchMap((profileRD) => this.researcherProfileService.updateByOrcidOperations(profileRD.payload, operations))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform unlink operation from ORCID profile.
|
||||||
|
*
|
||||||
|
* @param person The person item related to the researcher profile
|
||||||
|
*/
|
||||||
|
public unlinkOrcidByItem(person: Item): Observable<RemoteData<ResearcherProfile>> {
|
||||||
|
const operations: RemoveOperation[] = [{
|
||||||
|
path:'/orcid',
|
||||||
|
op:'remove'
|
||||||
|
}];
|
||||||
|
|
||||||
|
return this.researcherProfileService.findById(person.firstMetadata('dspace.object.owner').authority).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
switchMap((profileRD) => this.researcherProfileService.updateByOrcidOperations(profileRD.payload, operations))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build and return the url to authenticate with orcid
|
||||||
|
*
|
||||||
|
* @param profile
|
||||||
|
*/
|
||||||
|
public getOrcidAuthorizeUrl(profile: Item): Observable<string> {
|
||||||
|
return combineLatest([
|
||||||
|
this.configurationService.findByPropertyName('orcid.authorize-url').pipe(getFirstSucceededRemoteDataPayload()),
|
||||||
|
this.configurationService.findByPropertyName('orcid.application-client-id').pipe(getFirstSucceededRemoteDataPayload()),
|
||||||
|
this.configurationService.findByPropertyName('orcid.scope').pipe(getFirstSucceededRemoteDataPayload())]
|
||||||
|
).pipe(
|
||||||
|
map(([authorizeUrl, clientId, scopes]) => {
|
||||||
|
const redirectUri = new URLCombiner(this._window.nativeWindow.origin, encodeURIComponent(this.router.url.split('?')[0]));
|
||||||
|
console.log(redirectUri.toString());
|
||||||
|
return authorizeUrl.values[0] + '?client_id=' + clientId.values[0] + '&redirect_uri=' + redirectUri + '&response_type=code&scope='
|
||||||
|
+ scopes.values.join(' ');
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all orcid authorization scopes saved in the given item
|
||||||
|
*
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
public getOrcidAuthorizationScopesByItem(item: Item): string[] {
|
||||||
|
return isNotEmpty(item) ? item.allMetadataValues('dspace.orcid.scope') : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all orcid authorization scopes available by configuration
|
||||||
|
*/
|
||||||
|
public getOrcidAuthorizationScopes(): Observable<string[]> {
|
||||||
|
return this.configurationService.findByPropertyName('orcid.scope').pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((propertyRD: RemoteData<ConfigurationProperty>) => propertyRD.hasSucceeded ? propertyRD.payload.values : [])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOrcidDisconnectionAllowedUsersConfiguration(): Observable<RemoteData<ConfigurationProperty>> {
|
||||||
|
return this.configurationService.findByPropertyName('orcid.disconnection.allowed-users').pipe(
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
126
src/app/core/orcid/orcid-history-data.service.ts
Normal file
126
src/app/core/orcid/orcid-history-data.service.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
// eslint-disable-next-line max-classes-per-file
|
||||||
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { DataService } from '../data/data.service';
|
||||||
|
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
|
||||||
|
import { ItemDataService } from '../data/item-data.service';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { PostRequest } from '../data/request.models';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { OrcidHistory } from './model/orcid-history.model';
|
||||||
|
import { ORCID_HISTORY } from './model/orcid-history.resource-type';
|
||||||
|
import { OrcidQueue } from './model/orcid-queue.model';
|
||||||
|
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||||
|
import { CoreState } from '../core-state.model';
|
||||||
|
import { RestRequest } from '../data/rest-request.model';
|
||||||
|
import { sendRequest } from '../shared/request.operators';
|
||||||
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { FindListOptions } from '../data/find-list-options.model';
|
||||||
|
import { PaginatedList } from '../data/paginated-list.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A private DataService implementation to delegate specific methods to.
|
||||||
|
*/
|
||||||
|
class OrcidHistoryServiceImpl extends DataService<OrcidHistory> {
|
||||||
|
public linkPath = 'orcidhistories';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DefaultChangeAnalyzer<OrcidHistory>) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service that provides methods to make REST requests with Orcid History endpoint.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
@dataService(ORCID_HISTORY)
|
||||||
|
export class OrcidHistoryDataService {
|
||||||
|
|
||||||
|
dataService: OrcidHistoryServiceImpl;
|
||||||
|
|
||||||
|
responseMsToLive: number = 10 * 1000;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DefaultChangeAnalyzer<OrcidHistory>,
|
||||||
|
protected itemService: ItemDataService ) {
|
||||||
|
|
||||||
|
this.dataService = new OrcidHistoryServiceImpl(requestService, rdbService, store, objectCache, halService,
|
||||||
|
notificationsService, http, comparator);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
sendToORCID(orcidQueue: OrcidQueue): Observable<RemoteData<OrcidHistory>> {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
return this.getEndpoint().pipe(
|
||||||
|
map((endpointURL: string) => {
|
||||||
|
const options: HttpOptions = Object.create({});
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
headers = headers.append('Content-Type', 'text/uri-list');
|
||||||
|
options.headers = headers;
|
||||||
|
return new PostRequest(requestId, endpointURL, orcidQueue._links.self.href, options);
|
||||||
|
}),
|
||||||
|
sendRequest(this.requestService),
|
||||||
|
switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid) as Observable<RemoteData<OrcidHistory>>)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getEndpoint(): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(this.dataService.linkPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable of {@link RemoteData} of an object, based on its ID, with a list of
|
||||||
|
* {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object
|
||||||
|
* @param id ID of object we want to retrieve
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
|
* requested after the response becomes stale
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
||||||
|
* {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
findById(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<OrcidHistory>[]): Observable<RemoteData<OrcidHistory>> {
|
||||||
|
return this.dataService.findById(id, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of observables of {@link RemoteData} of {@link OrcidHistory}s, based on an href, with a list of {@link FollowLinkConfig},
|
||||||
|
* to automatically resolve {@link HALLink}s of the {@link OrcidHistory}
|
||||||
|
* @param href The url of object we want to retrieve
|
||||||
|
* @param findListOptions Find list options object
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
|
* requested after the response becomes stale
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
||||||
|
* {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
findAllByHref(href: string, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<OrcidHistory>[]): Observable<RemoteData<PaginatedList<OrcidHistory>>> {
|
||||||
|
return this.dataService.findAllByHref(href, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
110
src/app/core/orcid/orcid-queue.service.ts
Normal file
110
src/app/core/orcid/orcid-queue.service.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
// eslint-disable-next-line max-classes-per-file
|
||||||
|
import { DataService } from '../data/data.service';
|
||||||
|
import { OrcidQueue } from './model/orcid-queue.model';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
|
import { ORCID_QUEUE } from './model/orcid-queue.resource-type';
|
||||||
|
import { ItemDataService } from '../data/item-data.service';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { PaginatedList } from '../data/paginated-list.model';
|
||||||
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { NoContent } from '../shared/NoContent.model';
|
||||||
|
import { ConfigurationDataService } from '../data/configuration-data.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { CoreState } from '../core-state.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A private DataService implementation to delegate specific methods to.
|
||||||
|
*/
|
||||||
|
class OrcidQueueServiceImpl extends DataService<OrcidQueue> {
|
||||||
|
public linkPath = 'orcidqueues';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DefaultChangeAnalyzer<OrcidQueue>) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service that provides methods to make REST requests with Orcid Queue endpoint.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
@dataService(ORCID_QUEUE)
|
||||||
|
export class OrcidQueueService {
|
||||||
|
|
||||||
|
dataService: OrcidQueueServiceImpl;
|
||||||
|
|
||||||
|
responseMsToLive: number = 10 * 1000;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DefaultChangeAnalyzer<OrcidQueue>,
|
||||||
|
protected configurationService: ConfigurationDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected itemService: ItemDataService ) {
|
||||||
|
|
||||||
|
this.dataService = new OrcidQueueServiceImpl(requestService, rdbService, store, objectCache, halService,
|
||||||
|
notificationsService, http, comparator);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param itemId It represent an Id of profileItem
|
||||||
|
* @param paginationOptions The pagination options object
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
|
* requested after the response becomes stale
|
||||||
|
* @returns { OrcidQueue }
|
||||||
|
*/
|
||||||
|
searchByProfileItemId(itemId: string, paginationOptions: PaginationComponentOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true): Observable<RemoteData<PaginatedList<OrcidQueue>>> {
|
||||||
|
return this.dataService.searchBy('findByProfileItem', {
|
||||||
|
searchParams: [new RequestParam('profileItemId', itemId)],
|
||||||
|
elementsPerPage: paginationOptions.pageSize,
|
||||||
|
currentPage: paginationOptions.currentPage
|
||||||
|
},
|
||||||
|
useCachedVersionIfAvailable,
|
||||||
|
reRequestOnStale
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param orcidQueueId represents a id of orcid queue
|
||||||
|
* @returns { NoContent }
|
||||||
|
*/
|
||||||
|
deleteById(orcidQueueId: number): Observable<RemoteData<NoContent>> {
|
||||||
|
return this.dataService.delete(orcidQueueId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will set linkPath to stale
|
||||||
|
*/
|
||||||
|
clearFindByProfileItemRequests() {
|
||||||
|
this.requestService.setStaleByHrefSubstring(this.dataService.linkPath + '/search/findByProfileItem');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -12,7 +12,7 @@ describe('PaginationService', () => {
|
|||||||
let routeService;
|
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', () => {
|
||||||
|
@@ -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 = {};
|
||||||
|
|
||||||
|
@@ -26,6 +26,8 @@ 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 { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
|
|
||||||
describe('ResearcherProfileService', () => {
|
describe('ResearcherProfileService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
@@ -36,6 +38,7 @@ describe('ResearcherProfileService', () => {
|
|||||||
let objectCache: ObjectCacheService;
|
let objectCache: ObjectCacheService;
|
||||||
let halService: HALEndpointService;
|
let halService: HALEndpointService;
|
||||||
let responseCacheEntry: RequestEntry;
|
let responseCacheEntry: RequestEntry;
|
||||||
|
let routerStub: any;
|
||||||
|
|
||||||
const researcherProfileId = 'beef9946-rt56-479e-8f11-b90cbe9f7241';
|
const researcherProfileId = 'beef9946-rt56-479e-8f11-b90cbe9f7241';
|
||||||
const itemId = 'beef9946-rt56-479e-8f11-b90cbe9f7241';
|
const itemId = 'beef9946-rt56-479e-8f11-b90cbe9f7241';
|
||||||
@@ -86,6 +89,113 @@ describe('ResearcherProfileService', () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mockItemUnlinkedToOrcid: Item = Object.assign(new Item(), {
|
||||||
|
id: 'mockItemUnlinkedToOrcid',
|
||||||
|
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [{
|
||||||
|
value: 'test person'
|
||||||
|
}],
|
||||||
|
'dspace.entity.type': [{
|
||||||
|
'value': 'Person'
|
||||||
|
}],
|
||||||
|
'dspace.object.owner': [{
|
||||||
|
'value': 'test person',
|
||||||
|
'language': null,
|
||||||
|
'authority': 'researcher-profile-id',
|
||||||
|
'confidence': 600,
|
||||||
|
'place': 0
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockItemLinkedToOrcid: Item = Object.assign(new Item(), {
|
||||||
|
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [{
|
||||||
|
value: 'test person'
|
||||||
|
}],
|
||||||
|
'dspace.entity.type': [{
|
||||||
|
'value': 'Person'
|
||||||
|
}],
|
||||||
|
'dspace.object.owner': [{
|
||||||
|
'value': 'test person',
|
||||||
|
'language': null,
|
||||||
|
'authority': 'researcher-profile-id',
|
||||||
|
'confidence': 600,
|
||||||
|
'place': 0
|
||||||
|
}],
|
||||||
|
'dspace.orcid.authenticated': [{
|
||||||
|
'value': '2022-06-10T15:15:12.952872',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 0
|
||||||
|
}],
|
||||||
|
'dspace.orcid.scope': [{
|
||||||
|
'value': '/authenticate',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 0
|
||||||
|
}, {
|
||||||
|
'value': '/read-limited',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 1
|
||||||
|
}, {
|
||||||
|
'value': '/activities/update',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 2
|
||||||
|
}, {
|
||||||
|
'value': '/person/update',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 3
|
||||||
|
}],
|
||||||
|
'person.identifier.orcid': [{
|
||||||
|
'value': 'orcid-id',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 0
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const disconnectionAllowAdmin = {
|
||||||
|
uuid: 'orcid.disconnection.allowed-users',
|
||||||
|
name: 'orcid.disconnection.allowed-users',
|
||||||
|
values: ['only_admin']
|
||||||
|
} as ConfigurationProperty;
|
||||||
|
|
||||||
|
const disconnectionAllowAdminOwner = {
|
||||||
|
uuid: 'orcid.disconnection.allowed-users',
|
||||||
|
name: 'orcid.disconnection.allowed-users',
|
||||||
|
values: ['admin_and_owner']
|
||||||
|
} as ConfigurationProperty;
|
||||||
|
|
||||||
|
const authorizeUrl = {
|
||||||
|
uuid: 'orcid.authorize-url',
|
||||||
|
name: 'orcid.authorize-url',
|
||||||
|
values: ['orcid.authorize-url']
|
||||||
|
} as ConfigurationProperty;
|
||||||
|
const appClientId = {
|
||||||
|
uuid: 'orcid.application-client-id',
|
||||||
|
name: 'orcid.application-client-id',
|
||||||
|
values: ['orcid.application-client-id']
|
||||||
|
} as ConfigurationProperty;
|
||||||
|
const orcidScope = {
|
||||||
|
uuid: 'orcid.scope',
|
||||||
|
name: 'orcid.scope',
|
||||||
|
values: ['/authenticate', '/read-limited']
|
||||||
|
} as ConfigurationProperty;
|
||||||
|
|
||||||
const endpointURL = `https://rest.api/rest/api/profiles`;
|
const endpointURL = `https://rest.api/rest/api/profiles`;
|
||||||
const endpointURLWithEmbed = 'https://rest.api/rest/api/profiles?embed=item';
|
const endpointURLWithEmbed = 'https://rest.api/rest/api/profiles?embed=item';
|
||||||
const sourceUri = `https://rest.api/rest/api/external-source/profile`;
|
const sourceUri = `https://rest.api/rest/api/external-source/profile`;
|
||||||
@@ -132,7 +242,7 @@ describe('ResearcherProfileService', () => {
|
|||||||
const notificationsService = {} as NotificationsService;
|
const notificationsService = {} as NotificationsService;
|
||||||
const http = {} as HttpClient;
|
const http = {} as HttpClient;
|
||||||
const comparator = {} as any;
|
const comparator = {} as any;
|
||||||
const routerStub: any = new RouterMock();
|
routerStub = new RouterMock();
|
||||||
const itemService = jasmine.createSpyObj('ItemService', {
|
const itemService = jasmine.createSpyObj('ItemService', {
|
||||||
findByHref: jasmine.createSpy('findByHref')
|
findByHref: jasmine.createSpy('findByHref')
|
||||||
});
|
});
|
||||||
@@ -271,7 +381,7 @@ describe('ResearcherProfileService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('createFromExternalSource', () => {
|
describe('createFromExternalSource', () => {
|
||||||
let patchSpy;
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn((service as any).dataService, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
|
spyOn((service as any).dataService, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
|
||||||
spyOn((service as any), 'findById').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
|
spyOn((service as any), 'findById').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
|
||||||
@@ -293,4 +403,17 @@ describe('ResearcherProfileService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('updateByOrcidOperations', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
spyOn((service as any).dataService, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call patch method properly', () => {
|
||||||
|
scheduler.schedule(() => service.updateByOrcidOperations(researcherProfile, []).subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect((service as any).dataService.patch).toHaveBeenCalledWith(researcherProfile, []);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -4,10 +4,9 @@ import { Injectable } from '@angular/core';
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { ReplaceOperation } from 'fast-json-patch';
|
import { Operation, ReplaceOperation } from 'fast-json-patch';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
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 { 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';
|
||||||
@@ -24,10 +23,11 @@ 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 } 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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A private DataService implementation to delegate specific methods to.
|
* A private DataService implementation to delegate specific methods to.
|
||||||
@@ -56,9 +56,9 @@ class ResearcherProfileServiceImpl extends DataService<ResearcherProfile> {
|
|||||||
@dataService(RESEARCHER_PROFILE)
|
@dataService(RESEARCHER_PROFILE)
|
||||||
export class ResearcherProfileService {
|
export class ResearcherProfileService {
|
||||||
|
|
||||||
dataService: ResearcherProfileServiceImpl;
|
protected dataService: ResearcherProfileServiceImpl;
|
||||||
|
|
||||||
responseMsToLive: number = 10 * 1000;
|
protected responseMsToLive: number = 10 * 1000;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
@@ -112,6 +112,20 @@ export class ResearcherProfileService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a researcher profile by its own related item
|
||||||
|
*
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
public findByRelatedItem(item: Item): Observable<RemoteData<ResearcherProfile>> {
|
||||||
|
const profileId = item.firstMetadata('dspace.object.owner')?.authority;
|
||||||
|
if (isEmpty(profileId)) {
|
||||||
|
return createFailedRemoteDataObject$();
|
||||||
|
} else {
|
||||||
|
return this.findById(profileId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the item id related to the given researcher profile.
|
* Find the item id related to the given researcher profile.
|
||||||
*
|
*
|
||||||
@@ -164,4 +178,17 @@ export class ResearcherProfileService {
|
|||||||
|
|
||||||
return this.rdbService.buildFromRequestUUID(requestId, followLink('item'));
|
return this.rdbService.buildFromRequestUUID(requestId, followLink('item'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update researcher profile by patch orcid operation
|
||||||
|
*
|
||||||
|
* @param researcherProfile
|
||||||
|
* @param operations
|
||||||
|
*/
|
||||||
|
public updateByOrcidOperations(researcherProfile: ResearcherProfile, operations: Operation[]): Observable<RemoteData<ResearcherProfile>> {
|
||||||
|
return this.dataService.patch(researcherProfile, operations);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,9 @@
|
|||||||
{{'journalissue.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
{{'journalissue.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="pl-2 space-children-mr">
|
<div class="pl-2 space-children-mr">
|
||||||
|
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
|
||||||
|
[tooltipMsgCreate]="'item.page.version.create'"
|
||||||
|
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
|
||||||
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'journalissue.page.edit'"></ds-dso-page-edit-button>
|
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'journalissue.page.edit'"></ds-dso-page-edit-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component';
|
|
||||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
|
import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component';
|
||||||
|
|
||||||
@listableObjectComponent('JournalIssue', ViewMode.StandalonePage)
|
@listableObjectComponent('JournalIssue', ViewMode.StandalonePage)
|
||||||
@Component({
|
@Component({
|
||||||
@@ -12,5 +12,5 @@ import { listableObjectComponent } from '../../../../shared/object-collection/sh
|
|||||||
/**
|
/**
|
||||||
* The component for displaying metadata and relations of an item of the type Journal Issue
|
* The component for displaying metadata and relations of an item of the type Journal Issue
|
||||||
*/
|
*/
|
||||||
export class JournalIssueComponent extends ItemComponent {
|
export class JournalIssueComponent extends VersionedItemComponent {
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,9 @@
|
|||||||
{{'journalvolume.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
{{'journalvolume.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="pl-2 space-children-mr">
|
<div class="pl-2 space-children-mr">
|
||||||
|
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
|
||||||
|
[tooltipMsgCreate]="'item.page.version.create'"
|
||||||
|
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
|
||||||
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'journalvolume.page.edit'"></ds-dso-page-edit-button>
|
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'journalvolume.page.edit'"></ds-dso-page-edit-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component';
|
|
||||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
|
import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component';
|
||||||
|
|
||||||
@listableObjectComponent('JournalVolume', ViewMode.StandalonePage)
|
@listableObjectComponent('JournalVolume', ViewMode.StandalonePage)
|
||||||
@Component({
|
@Component({
|
||||||
@@ -12,5 +12,5 @@ import { listableObjectComponent } from '../../../../shared/object-collection/sh
|
|||||||
/**
|
/**
|
||||||
* The component for displaying metadata and relations of an item of the type Journal Volume
|
* The component for displaying metadata and relations of an item of the type Journal Volume
|
||||||
*/
|
*/
|
||||||
export class JournalVolumeComponent extends ItemComponent {
|
export class JournalVolumeComponent extends VersionedItemComponent {
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,9 @@
|
|||||||
{{'journal.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
{{'journal.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="pl-2 space-children-mr">
|
<div class="pl-2 space-children-mr">
|
||||||
|
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
|
||||||
|
[tooltipMsgCreate]="'item.page.version.create'"
|
||||||
|
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
|
||||||
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'journal.page.edit'"></ds-dso-page-edit-button>
|
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'journal.page.edit'"></ds-dso-page-edit-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,7 +47,8 @@
|
|||||||
<ds-tabbed-related-entities-search [item]="object"
|
<ds-tabbed-related-entities-search [item]="object"
|
||||||
[relationTypes]="[{
|
[relationTypes]="[{
|
||||||
label: 'isJournalOfPublication',
|
label: 'isJournalOfPublication',
|
||||||
filter: 'isJournalOfPublication'
|
filter: 'isJournalOfPublication',
|
||||||
|
configuration: 'default-relationships'
|
||||||
}]">
|
}]">
|
||||||
</ds-tabbed-related-entities-search>
|
</ds-tabbed-related-entities-search>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -29,6 +29,11 @@ import { TruncatableService } from '../../../../shared/truncatable/truncatable.s
|
|||||||
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
|
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
|
||||||
import { JournalComponent } from './journal.component';
|
import { JournalComponent } from './journal.component';
|
||||||
import { RouteService } from '../../../../core/services/route.service';
|
import { RouteService } from '../../../../core/services/route.service';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service';
|
||||||
|
import { VersionDataService } from '../../../../core/data/version-data.service';
|
||||||
|
import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service';
|
||||||
|
import { SearchService } from '../../../../core/shared/search/search.service';
|
||||||
|
|
||||||
let comp: JournalComponent;
|
let comp: JournalComponent;
|
||||||
let fixture: ComponentFixture<JournalComponent>;
|
let fixture: ComponentFixture<JournalComponent>;
|
||||||
@@ -65,12 +70,15 @@ describe('JournalComponent', () => {
|
|||||||
};
|
};
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot({
|
imports: [
|
||||||
loader: {
|
TranslateModule.forRoot({
|
||||||
provide: TranslateLoader,
|
loader: {
|
||||||
useClass: TranslateLoaderMock
|
provide: TranslateLoader,
|
||||||
}
|
useClass: TranslateLoaderMock
|
||||||
})],
|
}
|
||||||
|
}),
|
||||||
|
RouterTestingModule,
|
||||||
|
],
|
||||||
declarations: [JournalComponent, GenericItemPageFieldComponent, TruncatePipe],
|
declarations: [JournalComponent, GenericItemPageFieldComponent, TruncatePipe],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ItemDataService, useValue: {} },
|
{ provide: ItemDataService, useValue: {} },
|
||||||
@@ -86,7 +94,11 @@ describe('JournalComponent', () => {
|
|||||||
{ provide: DSOChangeAnalyzer, useValue: {} },
|
{ provide: DSOChangeAnalyzer, useValue: {} },
|
||||||
{ provide: NotificationsService, useValue: {} },
|
{ provide: NotificationsService, useValue: {} },
|
||||||
{ provide: DefaultChangeAnalyzer, useValue: {} },
|
{ provide: DefaultChangeAnalyzer, useValue: {} },
|
||||||
|
{ provide: VersionHistoryDataService, useValue: {} },
|
||||||
|
{ provide: VersionDataService, useValue: {} },
|
||||||
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
||||||
|
{ provide: WorkspaceitemDataService, useValue: {} },
|
||||||
|
{ provide: SearchService, useValue: {} },
|
||||||
{ provide: RouteService, useValue: {} }
|
{ provide: RouteService, useValue: {} }
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component';
|
|
||||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
|
import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component';
|
||||||
|
|
||||||
@listableObjectComponent('Journal', ViewMode.StandalonePage)
|
@listableObjectComponent('Journal', ViewMode.StandalonePage)
|
||||||
@Component({
|
@Component({
|
||||||
@@ -12,5 +12,5 @@ import { listableObjectComponent } from '../../../../shared/object-collection/sh
|
|||||||
/**
|
/**
|
||||||
* The component for displaying metadata and relations of an item of the type Journal
|
* The component for displaying metadata and relations of an item of the type Journal
|
||||||
*/
|
*/
|
||||||
export class JournalComponent extends ItemComponent {
|
export class JournalComponent extends VersionedItemComponent {
|
||||||
}
|
}
|
||||||
|
@@ -7,13 +7,11 @@
|
|||||||
class="lead"
|
class="lead"
|
||||||
[innerHTML]="firstMetadataValue('organization.legalName')"></span>
|
[innerHTML]="firstMetadataValue('organization.legalName')"></span>
|
||||||
<span class="text-muted">
|
<span class="text-muted">
|
||||||
<ds-truncatable-part [id]="dso.id" [minLines]="3">
|
<span *ngIf="dso.allMetadata(['dc.description']).length > 0"
|
||||||
<span *ngIf="dso.allMetadata(['dc.description']).length > 0"
|
class="item-list-org-unit-description">
|
||||||
class="item-list-org-unit-description">
|
<ds-truncatable-part [id]="dso.id" [minLines]="3"><span
|
||||||
<ds-truncatable-part [id]="dso.id" [minLines]="3"><span
|
[innerHTML]="firstMetadataValue('dc.description')"></span>
|
||||||
[innerHTML]="firstMetadataValue('dc.description')"></span>
|
</ds-truncatable-part>
|
||||||
</ds-truncatable-part>
|
</span>
|
||||||
</span>
|
|
||||||
</ds-truncatable-part>
|
|
||||||
</span>
|
</span>
|
||||||
</ds-truncatable>
|
</ds-truncatable>
|
||||||
|
@@ -3,6 +3,9 @@
|
|||||||
{{'orgunit.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['organization.legalName'])"></ds-metadata-values>
|
{{'orgunit.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['organization.legalName'])"></ds-metadata-values>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="pl-2 space-children-mr">
|
<div class="pl-2 space-children-mr">
|
||||||
|
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
|
||||||
|
[tooltipMsgCreate]="'item.page.version.create'"
|
||||||
|
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
|
||||||
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'orgunit.page.edit'"></ds-dso-page-edit-button>
|
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'orgunit.page.edit'"></ds-dso-page-edit-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,12 +57,12 @@
|
|||||||
[relationTypes]="[{
|
[relationTypes]="[{
|
||||||
label: 'isOrgUnitOfPerson',
|
label: 'isOrgUnitOfPerson',
|
||||||
filter: 'isOrgUnitOfPerson',
|
filter: 'isOrgUnitOfPerson',
|
||||||
configuration: 'person'
|
configuration: 'person-relationships'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'isOrgUnitOfProject',
|
label: 'isOrgUnitOfProject',
|
||||||
filter: 'isOrgUnitOfProject',
|
filter: 'isOrgUnitOfProject',
|
||||||
configuration: 'project'
|
configuration: 'project-relationships'
|
||||||
}]">
|
}]">
|
||||||
</ds-tabbed-related-entities-search>
|
</ds-tabbed-related-entities-search>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component';
|
|
||||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
|
import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component';
|
||||||
|
|
||||||
@listableObjectComponent('OrgUnit', ViewMode.StandalonePage)
|
@listableObjectComponent('OrgUnit', ViewMode.StandalonePage)
|
||||||
@Component({
|
@Component({
|
||||||
@@ -12,5 +12,5 @@ import { listableObjectComponent } from '../../../../shared/object-collection/sh
|
|||||||
/**
|
/**
|
||||||
* The component for displaying metadata and relations of an item of the type Organisation Unit
|
* The component for displaying metadata and relations of an item of the type Organisation Unit
|
||||||
*/
|
*/
|
||||||
export class OrgUnitComponent extends ItemComponent {
|
export class OrgUnitComponent extends VersionedItemComponent {
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,10 @@
|
|||||||
{{'person.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="getTitleMetadataValues()" [separator]="', '"></ds-metadata-values>
|
{{'person.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="getTitleMetadataValues()" [separator]="', '"></ds-metadata-values>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="pl-2 space-children-mr">
|
<div class="pl-2 space-children-mr">
|
||||||
|
<ds-dso-page-orcid-button [pageRoute]="itemPageRoute" [dso]="object" class="mr-2"></ds-dso-page-orcid-button>
|
||||||
|
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
|
||||||
|
[tooltipMsgCreate]="'item.page.version.create'"
|
||||||
|
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
|
||||||
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'person.page.edit'"></ds-dso-page-edit-button>
|
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'person.page.edit'"></ds-dso-page-edit-button>
|
||||||
<ds-person-page-claim-button [object]="object"></ds-person-page-claim-button>
|
<ds-person-page-claim-button [object]="object"></ds-person-page-claim-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,7 +66,8 @@
|
|||||||
<ds-tabbed-related-entities-search [item]="object"
|
<ds-tabbed-related-entities-search [item]="object"
|
||||||
[relationTypes]="[{
|
[relationTypes]="[{
|
||||||
label: 'isAuthorOfPublication',
|
label: 'isAuthorOfPublication',
|
||||||
filter: 'isAuthorOfPublication'
|
filter: 'isAuthorOfPublication',
|
||||||
|
configuration: 'default-relationships'
|
||||||
}]">
|
}]">
|
||||||
</ds-tabbed-related-entities-search>
|
</ds-tabbed-related-entities-search>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,9 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component';
|
|
||||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
import {
|
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
listableObjectComponent
|
import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component';
|
||||||
} from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
|
||||||
import { MetadataValue } from '../../../../core/shared/metadata.models';
|
import { MetadataValue } from '../../../../core/shared/metadata.models';
|
||||||
|
|
||||||
@listableObjectComponent('Person', ViewMode.StandalonePage)
|
@listableObjectComponent('Person', ViewMode.StandalonePage)
|
||||||
@@ -15,7 +13,7 @@ import { MetadataValue } from '../../../../core/shared/metadata.models';
|
|||||||
/**
|
/**
|
||||||
* The component for displaying metadata and relations of an item of the type Person
|
* The component for displaying metadata and relations of an item of the type Person
|
||||||
*/
|
*/
|
||||||
export class PersonComponent extends ItemComponent {
|
export class PersonComponent extends VersionedItemComponent {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the metadata values to be used for the page title.
|
* Returns the metadata values to be used for the page title.
|
||||||
@@ -36,4 +34,5 @@ export class PersonComponent extends ItemComponent {
|
|||||||
}
|
}
|
||||||
return metadataValues;
|
return metadataValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,9 @@
|
|||||||
{{'project.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
{{'project.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="pl-2 space-children-mr">
|
<div class="pl-2 space-children-mr">
|
||||||
|
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
|
||||||
|
[tooltipMsgCreate]="'item.page.version.create'"
|
||||||
|
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
|
||||||
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'project.page.edit'"></ds-dso-page-edit-button>
|
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'project.page.edit'"></ds-dso-page-edit-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component';
|
|
||||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
|
import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component';
|
||||||
|
|
||||||
@listableObjectComponent('Project', ViewMode.StandalonePage)
|
@listableObjectComponent('Project', ViewMode.StandalonePage)
|
||||||
@Component({
|
@Component({
|
||||||
@@ -12,5 +12,5 @@ import { listableObjectComponent } from '../../../../shared/object-collection/sh
|
|||||||
/**
|
/**
|
||||||
* The component for displaying metadata and relations of an item of the type Project
|
* The component for displaying metadata and relations of an item of the type Project
|
||||||
*/
|
*/
|
||||||
export class ProjectComponent extends ItemComponent {
|
export class ProjectComponent extends VersionedItemComponent {
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbTooltipModule, NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
import { SharedModule } from '../../shared/shared.module';
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
import { EditItemPageRoutingModule } from './edit-item-page.routing.module';
|
import { EditItemPageRoutingModule } from './edit-item-page.routing.module';
|
||||||
@@ -48,7 +48,8 @@ import { ResourcePoliciesModule } from '../../shared/resource-policies/resource-
|
|||||||
EditItemPageRoutingModule,
|
EditItemPageRoutingModule,
|
||||||
SearchPageModule,
|
SearchPageModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
ResourcePoliciesModule
|
ResourcePoliciesModule,
|
||||||
|
NgbModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
EditItemPageComponent,
|
EditItemPageComponent,
|
||||||
|
@@ -1,13 +1,33 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<ds-alert [type]="'alert-info'" [content]="'item.edit.authorizations.heading'"></ds-alert>
|
<ds-alert [type]="'alert-info'" [content]="'item.edit.authorizations.heading'"></ds-alert>
|
||||||
<ds-resource-policies [resourceType]="'item'" [resourceUUID]="(getItemUUID() | async)"></ds-resource-policies>
|
<ds-resource-policies [resourceType]="'item'" [resourceName]="(getItemName() | async)"
|
||||||
<ng-container *ngFor="let bundle of (getItemBundles() | async); trackById">
|
[resourceUUID]="(getItemUUID() | async)">
|
||||||
<ds-resource-policies [resourceType]="'bundle'"
|
</ds-resource-policies>
|
||||||
[resourceUUID]="bundle.id"></ds-resource-policies>
|
<ng-container *ngFor="let bundle of (bundles$ | async); trackById">
|
||||||
<ng-container *ngFor="let bitstream of (bundleBitstreamsMap.get(bundle.id) | async)?.page; trackById">
|
<ds-resource-policies [resourceType]="'bundle'" [resourceUUID]="bundle.id" [resourceName]="bundle.name">
|
||||||
<ds-resource-policies [resourceType]="'bitstream'"
|
</ds-resource-policies>
|
||||||
[resourceUUID]="bitstream.id"></ds-resource-policies>
|
<ng-container *ngIf="(bundleBitstreamsMap.get(bundle.id)?.bitstreams | async)?.length > 0">
|
||||||
|
<div class="card auth-bitstream-container">
|
||||||
|
<div class="card-header">
|
||||||
|
<button type="button" class="btn btn-outline-primary" (click)="collapseArea(bundle.id)"
|
||||||
|
[attr.aria-expanded]="false" [attr.aria-controls]="bundle.id">
|
||||||
|
{{ 'collection.edit.item.authorizations.show-bitstreams-button' | translate }} {{bundle.name}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" [id]="bundle.id" [ngbCollapse]="bundleBitstreamsMap.get(bundle.id).isCollapsed">
|
||||||
|
<ng-container
|
||||||
|
*ngFor="let bitstream of (bundleBitstreamsMap.get(bundle.id).bitstreams | async); trackById">
|
||||||
|
<ds-resource-policies [resourceType]="'bitstream'" [resourceUUID]="bitstream.id"
|
||||||
|
[resourceName]="bitstream.name"></ds-resource-policies>
|
||||||
|
</ng-container>
|
||||||
|
<div class="row justify-content-center" *ngIf="!bundleBitstreamsMap.get(bundle.id).allBitstreamsLoaded">
|
||||||
|
<button type="button" class="btn btn-link" (click)="onBitstreamsLoad(bundle)">{{ 'collection.edit.item.authorizations.load-more-button' | translate }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
<div class="row justify-content-center" *ngIf="!allBundlesLoaded">
|
||||||
|
<button type="button" class="btn btn-link" (click)="onBundleLoad()">{{ 'collection.edit.item.authorizations.load-bundle-button' | translate }}</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
.auth-bitstream-container {
|
||||||
|
margin-top: -1em;
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
@@ -1,11 +1,12 @@
|
|||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { waitForAsync, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
import { waitForAsync, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||||
import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf, of } from 'rxjs';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { cold } from 'jasmine-marbles';
|
import { cold } from 'jasmine-marbles';
|
||||||
import { ItemAuthorizationsComponent } from './item-authorizations.component';
|
import { ItemAuthorizationsComponent, BitstreamMapValue } from './item-authorizations.component';
|
||||||
import { Bitstream } from '../../../core/shared/bitstream.model';
|
import { Bitstream } from '../../../core/shared/bitstream.model';
|
||||||
import { Bundle } from '../../../core/shared/bundle.model';
|
import { Bundle } from '../../../core/shared/bundle.model';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
@@ -57,8 +58,6 @@ describe('ItemAuthorizationsComponent test suite', () => {
|
|||||||
bitstreams: createSuccessfulRemoteDataObject$(createPaginatedList([bitstream3, bitstream4]))
|
bitstreams: createSuccessfulRemoteDataObject$(createPaginatedList([bitstream3, bitstream4]))
|
||||||
});
|
});
|
||||||
const bundles = [bundle1, bundle2];
|
const bundles = [bundle1, bundle2];
|
||||||
const bitstreamList1: PaginatedList<Bitstream> = buildPaginatedList(new PageInfo(), [bitstream1, bitstream2]);
|
|
||||||
const bitstreamList2: PaginatedList<Bitstream> = buildPaginatedList(new PageInfo(), [bitstream3, bitstream4]);
|
|
||||||
|
|
||||||
const item = Object.assign(new Item(), {
|
const item = Object.assign(new Item(), {
|
||||||
uuid: 'item',
|
uuid: 'item',
|
||||||
@@ -142,13 +141,12 @@ describe('ItemAuthorizationsComponent test suite', () => {
|
|||||||
expect(compAsAny.bundleBitstreamsMap.has('bundle1')).toBeTruthy();
|
expect(compAsAny.bundleBitstreamsMap.has('bundle1')).toBeTruthy();
|
||||||
expect(compAsAny.bundleBitstreamsMap.has('bundle2')).toBeTruthy();
|
expect(compAsAny.bundleBitstreamsMap.has('bundle2')).toBeTruthy();
|
||||||
let bitstreamList = compAsAny.bundleBitstreamsMap.get('bundle1');
|
let bitstreamList = compAsAny.bundleBitstreamsMap.get('bundle1');
|
||||||
expect(bitstreamList).toBeObservable(cold('(a|)', {
|
expect(bitstreamList.bitstreams).toBeObservable(cold('(a|)', {
|
||||||
a: bitstreamList1
|
a : [bitstream1, bitstream2]
|
||||||
}));
|
}));
|
||||||
|
|
||||||
bitstreamList = compAsAny.bundleBitstreamsMap.get('bundle2');
|
bitstreamList = compAsAny.bundleBitstreamsMap.get('bundle2');
|
||||||
expect(bitstreamList).toBeObservable(cold('(a|)', {
|
expect(bitstreamList.bitstreams).toBeObservable(cold('(a|)', {
|
||||||
a: bitstreamList2
|
a: [bitstream3, bitstream4]
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import { isEqual } from 'lodash';
|
||||||
|
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
@@ -6,7 +8,8 @@ import { catchError, filter, first, map, mergeMap, take } from 'rxjs/operators';
|
|||||||
|
|
||||||
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
|
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
import {
|
import {
|
||||||
getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteDataWithNotEmptyPayload,
|
getFirstSucceededRemoteDataPayload,
|
||||||
|
getFirstSucceededRemoteDataWithNotEmptyPayload,
|
||||||
} from '../../../core/shared/operators';
|
} from '../../../core/shared/operators';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
@@ -25,7 +28,8 @@ interface BundleBitstreamsMapEntry {
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-authorizations',
|
selector: 'ds-item-authorizations',
|
||||||
templateUrl: './item-authorizations.component.html'
|
templateUrl: './item-authorizations.component.html',
|
||||||
|
styleUrls:['./item-authorizations.component.scss']
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* Component that handles the item Authorizations
|
* Component that handles the item Authorizations
|
||||||
@@ -36,13 +40,13 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
|
|||||||
* A map that contains all bitstream of the item's bundles
|
* A map that contains all bitstream of the item's bundles
|
||||||
* @type {Observable<Map<string, Observable<PaginatedList<Bitstream>>>>}
|
* @type {Observable<Map<string, Observable<PaginatedList<Bitstream>>>>}
|
||||||
*/
|
*/
|
||||||
public bundleBitstreamsMap: Map<string, Observable<PaginatedList<Bitstream>>> = new Map<string, Observable<PaginatedList<Bitstream>>>();
|
public bundleBitstreamsMap: Map<string, BitstreamMapValue> = new Map<string, BitstreamMapValue>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The list of bundle for the item
|
* The list of all bundles for the item
|
||||||
* @type {Observable<PaginatedList<Bundle>>}
|
* @type {Observable<PaginatedList<Bundle>>}
|
||||||
*/
|
*/
|
||||||
private bundles$: BehaviorSubject<Bundle[]> = new BehaviorSubject<Bundle[]>([]);
|
bundles$: BehaviorSubject<Bundle[]> = new BehaviorSubject<Bundle[]>([]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The target editing item
|
* The target editing item
|
||||||
@@ -56,15 +60,48 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
private subs: Subscription[] = [];
|
private subs: Subscription[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The size of the bundles to be loaded on demand
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
bundlesPerPage = 6;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of current page
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
bundlesPageSize = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The flag to show or not the 'Load more' button
|
||||||
|
* based on the condition if all the bundles are loaded or not
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
allBundlesLoaded = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial size of loaded bitstreams
|
||||||
|
* The size of incrementation used in bitstream pagination
|
||||||
|
*/
|
||||||
|
bitstreamSize = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The size of the loaded bitstremas at a certain moment
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private bitstreamPageSize = 4;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize instance variables
|
* Initialize instance variables
|
||||||
*
|
*
|
||||||
* @param {LinkService} linkService
|
* @param {LinkService} linkService
|
||||||
* @param {ActivatedRoute} route
|
* @param {ActivatedRoute} route
|
||||||
|
* @param nameService
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private linkService: LinkService,
|
private linkService: LinkService,
|
||||||
private route: ActivatedRoute
|
private route: ActivatedRoute,
|
||||||
|
private nameService: DSONameService
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,16 +109,53 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
|
|||||||
* Initialize the component, setting up the bundle and bitstream within the item
|
* Initialize the component, setting up the bundle and bitstream within the item
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.getBundlesPerItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the item's UUID
|
||||||
|
*/
|
||||||
|
getItemUUID(): Observable<string> {
|
||||||
|
return this.item$.pipe(
|
||||||
|
map((item: Item) => item.id),
|
||||||
|
first((UUID: string) => isNotEmpty(UUID))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the item's name
|
||||||
|
*/
|
||||||
|
getItemName(): Observable<string> {
|
||||||
|
return this.item$.pipe(
|
||||||
|
map((item: Item) => this.nameService.getName(item))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all item's bundles
|
||||||
|
*
|
||||||
|
* @return an observable that emits all item's bundles
|
||||||
|
*/
|
||||||
|
getItemBundles(): Observable<Bundle[]> {
|
||||||
|
return this.bundles$.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all bundles per item
|
||||||
|
* and all the bitstreams per bundle
|
||||||
|
* @param page number of current page
|
||||||
|
*/
|
||||||
|
getBundlesPerItem(page: number = 1) {
|
||||||
this.item$ = this.route.data.pipe(
|
this.item$ = this.route.data.pipe(
|
||||||
map((data) => data.dso),
|
map((data) => data.dso),
|
||||||
getFirstSucceededRemoteDataWithNotEmptyPayload(),
|
getFirstSucceededRemoteDataWithNotEmptyPayload(),
|
||||||
map((item: Item) => this.linkService.resolveLink(
|
map((item: Item) => this.linkService.resolveLink(
|
||||||
item,
|
item,
|
||||||
followLink('bundles', {}, followLink('bitstreams'))
|
followLink('bundles', {findListOptions: {currentPage : page, elementsPerPage: this.bundlesPerPage}}, followLink('bitstreams'))
|
||||||
))
|
))
|
||||||
) as Observable<Item>;
|
) as Observable<Item>;
|
||||||
|
|
||||||
const bundles$: Observable<PaginatedList<Bundle>> = this.item$.pipe(
|
const bundles$: Observable<PaginatedList<Bundle>> = this.item$.pipe(
|
||||||
filter((item: Item) => isNotEmpty(item.bundles)),
|
filter((item: Item) => isNotEmpty(item.bundles)),
|
||||||
mergeMap((item: Item) => item.bundles),
|
mergeMap((item: Item) => item.bundles),
|
||||||
getFirstSucceededRemoteDataWithNotEmptyPayload(),
|
getFirstSucceededRemoteDataWithNotEmptyPayload(),
|
||||||
@@ -96,37 +170,36 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
|
|||||||
take(1),
|
take(1),
|
||||||
map((list: PaginatedList<Bundle>) => list.page)
|
map((list: PaginatedList<Bundle>) => list.page)
|
||||||
).subscribe((bundles: Bundle[]) => {
|
).subscribe((bundles: Bundle[]) => {
|
||||||
this.bundles$.next(bundles);
|
if (isEqual(bundles.length,0) || bundles.length < this.bundlesPerPage) {
|
||||||
|
this.allBundlesLoaded = true;
|
||||||
|
}
|
||||||
|
if (isEqual(page, 1)) {
|
||||||
|
this.bundles$.next(bundles);
|
||||||
|
} else {
|
||||||
|
this.bundles$.next(this.bundles$.getValue().concat(bundles));
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
bundles$.pipe(
|
bundles$.pipe(
|
||||||
take(1),
|
take(1),
|
||||||
mergeMap((list: PaginatedList<Bundle>) => list.page),
|
mergeMap((list: PaginatedList<Bundle>) => list.page),
|
||||||
map((bundle: Bundle) => ({ id: bundle.id, bitstreams: this.getBundleBitstreams(bundle) }))
|
map((bundle: Bundle) => ({ id: bundle.id, bitstreams: this.getBundleBitstreams(bundle) }))
|
||||||
).subscribe((entry: BundleBitstreamsMapEntry) => {
|
).subscribe((entry: BundleBitstreamsMapEntry) => {
|
||||||
this.bundleBitstreamsMap.set(entry.id, entry.bitstreams);
|
let bitstreamMapValues: BitstreamMapValue = {
|
||||||
|
isCollapsed: true,
|
||||||
|
allBitstreamsLoaded: false,
|
||||||
|
bitstreams: null
|
||||||
|
};
|
||||||
|
bitstreamMapValues.bitstreams = entry.bitstreams.pipe(
|
||||||
|
map((b: PaginatedList<Bitstream>) => {
|
||||||
|
bitstreamMapValues.allBitstreamsLoaded = b?.page.length < this.bitstreamSize;
|
||||||
|
return [...b.page.slice(0, this.bitstreamSize)];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.bundleBitstreamsMap.set(entry.id, bitstreamMapValues);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the item's UUID
|
|
||||||
*/
|
|
||||||
getItemUUID(): Observable<string> {
|
|
||||||
return this.item$.pipe(
|
|
||||||
map((item: Item) => item.id),
|
|
||||||
first((UUID: string) => isNotEmpty(UUID))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return all item's bundles
|
|
||||||
*
|
|
||||||
* @return an observable that emits all item's bundles
|
|
||||||
*/
|
|
||||||
getItemBundles(): Observable<Bundle[]> {
|
|
||||||
return this.bundles$.asObservable();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return all bundle's bitstreams
|
* Return all bundle's bitstreams
|
||||||
*
|
*
|
||||||
@@ -142,6 +215,46 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the collapsible state of the area that contains the bitstream list
|
||||||
|
* @param bundleId Id of bundle responsible for the requested bitstreams
|
||||||
|
*/
|
||||||
|
collapseArea(bundleId: string) {
|
||||||
|
this.bundleBitstreamsMap.get(bundleId).isCollapsed = !this.bundleBitstreamsMap.get(bundleId).isCollapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads as much bundles as initial value of bundleSize to be displayed
|
||||||
|
*/
|
||||||
|
onBundleLoad(){
|
||||||
|
this.bundlesPageSize ++;
|
||||||
|
this.getBundlesPerItem(this.bundlesPageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the bitstreams that are going to be loaded on demand,
|
||||||
|
* based on the number configured on this.bitstreamSize.
|
||||||
|
* @param bundle parent of bitstreams that are requested to be shown
|
||||||
|
* @returns Subscription
|
||||||
|
*/
|
||||||
|
onBitstreamsLoad(bundle: Bundle) {
|
||||||
|
return this.getBundleBitstreams(bundle).subscribe((res: PaginatedList<Bitstream>) => {
|
||||||
|
let nextBitstreams = res?.page.slice(this.bitstreamPageSize, this.bitstreamPageSize + this.bitstreamSize);
|
||||||
|
let bitstreamsToShow = this.bundleBitstreamsMap.get(bundle.id).bitstreams.pipe(
|
||||||
|
map((existingBits: Bitstream[])=> {
|
||||||
|
return [... existingBits, ...nextBitstreams];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.bitstreamPageSize = this.bitstreamPageSize + this.bitstreamSize;
|
||||||
|
let bitstreamMapValues: BitstreamMapValue = {
|
||||||
|
bitstreams: bitstreamsToShow ,
|
||||||
|
isCollapsed: this.bundleBitstreamsMap.get(bundle.id).isCollapsed,
|
||||||
|
allBitstreamsLoaded: res?.page.length <= this.bitstreamPageSize
|
||||||
|
};
|
||||||
|
this.bundleBitstreamsMap.set(bundle.id, bitstreamMapValues);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unsubscribe from all subscriptions
|
* Unsubscribe from all subscriptions
|
||||||
*/
|
*/
|
||||||
@@ -151,3 +264,9 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
|
|||||||
.forEach((subscription) => subscription.unsubscribe());
|
.forEach((subscription) => subscription.unsubscribe());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BitstreamMapValue {
|
||||||
|
bitstreams: Observable<Bitstream[]>;
|
||||||
|
isCollapsed: boolean;
|
||||||
|
allBitstreamsLoaded: boolean;
|
||||||
|
}
|
||||||
|
@@ -50,3 +50,7 @@
|
|||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .larger-tooltip .tooltip-inner {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
@@ -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"
|
||||||
{{ bitstream?.firstMetadataValue('dc.description') }}
|
[ngbTooltip]="bitstream?.firstMetadataValue('dc.description')">
|
||||||
</span>
|
{{ bitstream?.firstMetadataValue('dc.description') }}
|
||||||
|
</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">
|
||||||
|
@@ -50,3 +50,4 @@ export const ITEM_EDIT_PATH = 'edit';
|
|||||||
export const ITEM_EDIT_VERSIONHISTORY_PATH = 'versionhistory';
|
export const ITEM_EDIT_VERSIONHISTORY_PATH = 'versionhistory';
|
||||||
export const ITEM_VERSION_PATH = 'version';
|
export const ITEM_VERSION_PATH = 'version';
|
||||||
export const UPLOAD_BITSTREAM_PATH = 'bitstreams/new';
|
export const UPLOAD_BITSTREAM_PATH = 'bitstreams/new';
|
||||||
|
export const ORCID_PATH = 'orcid';
|
||||||
|
@@ -7,15 +7,19 @@ import { VersionResolver } from './version-page/version.resolver';
|
|||||||
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
||||||
import { LinkService } from '../core/cache/builders/link.service';
|
import { LinkService } from '../core/cache/builders/link.service';
|
||||||
import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component';
|
import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component';
|
||||||
import { ITEM_EDIT_PATH, UPLOAD_BITSTREAM_PATH } from './item-page-routing-paths';
|
import { ITEM_EDIT_PATH, ORCID_PATH, UPLOAD_BITSTREAM_PATH } from './item-page-routing-paths';
|
||||||
import { ItemPageAdministratorGuard } from './item-page-administrator.guard';
|
import { ItemPageAdministratorGuard } from './item-page-administrator.guard';
|
||||||
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||||
import { ThemedItemPageComponent } from './simple/themed-item-page.component';
|
import { ThemedItemPageComponent } from './simple/themed-item-page.component';
|
||||||
import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component';
|
import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component';
|
||||||
import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
||||||
import { VersionPageComponent } from './version-page/version-page/version-page.component';
|
import { VersionPageComponent } from './version-page/version-page/version-page.component';
|
||||||
import { BitstreamRequestACopyPageComponent } from '../shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component';
|
import {
|
||||||
|
BitstreamRequestACopyPageComponent
|
||||||
|
} from '../shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component';
|
||||||
import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths';
|
import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths';
|
||||||
|
import { OrcidPageComponent } from './orcid-page/orcid-page.component';
|
||||||
|
import { OrcidPageGuard } from './orcid-page/orcid-page.guard';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -50,6 +54,11 @@ import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths';
|
|||||||
{
|
{
|
||||||
path: REQUEST_COPY_MODULE_PATH,
|
path: REQUEST_COPY_MODULE_PATH,
|
||||||
component: BitstreamRequestACopyPageComponent,
|
component: BitstreamRequestACopyPageComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ORCID_PATH,
|
||||||
|
component: OrcidPageComponent,
|
||||||
|
canActivate: [AuthenticatedGuard, OrcidPageGuard]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
data: {
|
data: {
|
||||||
@@ -88,6 +97,7 @@ import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths';
|
|||||||
LinkService,
|
LinkService,
|
||||||
ItemPageAdministratorGuard,
|
ItemPageAdministratorGuard,
|
||||||
VersionResolver,
|
VersionResolver,
|
||||||
|
OrcidPageGuard
|
||||||
]
|
]
|
||||||
|
|
||||||
})
|
})
|
||||||
|
@@ -6,11 +6,19 @@ import { SharedModule } from '../shared/shared.module';
|
|||||||
import { ItemPageComponent } from './simple/item-page.component';
|
import { ItemPageComponent } from './simple/item-page.component';
|
||||||
import { ItemPageRoutingModule } from './item-page-routing.module';
|
import { ItemPageRoutingModule } from './item-page-routing.module';
|
||||||
import { MetadataUriValuesComponent } from './field-components/metadata-uri-values/metadata-uri-values.component';
|
import { MetadataUriValuesComponent } from './field-components/metadata-uri-values/metadata-uri-values.component';
|
||||||
import { ItemPageAuthorFieldComponent } from './simple/field-components/specific-field/author/item-page-author-field.component';
|
import {
|
||||||
import { ItemPageDateFieldComponent } from './simple/field-components/specific-field/date/item-page-date-field.component';
|
ItemPageAuthorFieldComponent
|
||||||
import { ItemPageAbstractFieldComponent } from './simple/field-components/specific-field/abstract/item-page-abstract-field.component';
|
} from './simple/field-components/specific-field/author/item-page-author-field.component';
|
||||||
|
import {
|
||||||
|
ItemPageDateFieldComponent
|
||||||
|
} from './simple/field-components/specific-field/date/item-page-date-field.component';
|
||||||
|
import {
|
||||||
|
ItemPageAbstractFieldComponent
|
||||||
|
} from './simple/field-components/specific-field/abstract/item-page-abstract-field.component';
|
||||||
import { ItemPageUriFieldComponent } from './simple/field-components/specific-field/uri/item-page-uri-field.component';
|
import { ItemPageUriFieldComponent } from './simple/field-components/specific-field/uri/item-page-uri-field.component';
|
||||||
import { ItemPageTitleFieldComponent } from './simple/field-components/specific-field/title/item-page-title-field.component';
|
import {
|
||||||
|
ItemPageTitleFieldComponent
|
||||||
|
} from './simple/field-components/specific-field/title/item-page-title-field.component';
|
||||||
import { ItemPageFieldComponent } from './simple/field-components/specific-field/item-page-field.component';
|
import { ItemPageFieldComponent } from './simple/field-components/specific-field/item-page-field.component';
|
||||||
import { CollectionsComponent } from './field-components/collections/collections.component';
|
import { CollectionsComponent } from './field-components/collections/collections.component';
|
||||||
import { FullItemPageComponent } from './full/full-item-page.component';
|
import { FullItemPageComponent } from './full/full-item-page.component';
|
||||||
@@ -20,7 +28,9 @@ import { ItemComponent } from './simple/item-types/shared/item.component';
|
|||||||
import { EditItemPageModule } from './edit-item-page/edit-item-page.module';
|
import { EditItemPageModule } from './edit-item-page/edit-item-page.module';
|
||||||
import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component';
|
import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component';
|
||||||
import { StatisticsModule } from '../statistics/statistics.module';
|
import { StatisticsModule } from '../statistics/statistics.module';
|
||||||
import { AbstractIncrementalListComponent } from './simple/abstract-incremental-list/abstract-incremental-list.component';
|
import {
|
||||||
|
AbstractIncrementalListComponent
|
||||||
|
} from './simple/abstract-incremental-list/abstract-incremental-list.component';
|
||||||
import { UntypedItemComponent } from './simple/item-types/untyped-item/untyped-item.component';
|
import { UntypedItemComponent } from './simple/item-types/untyped-item/untyped-item.component';
|
||||||
import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module';
|
import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module';
|
||||||
import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module';
|
import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module';
|
||||||
@@ -34,6 +44,11 @@ import { MiradorViewerComponent } from './mirador-viewer/mirador-viewer.componen
|
|||||||
import { VersionPageComponent } from './version-page/version-page/version-page.component';
|
import { VersionPageComponent } from './version-page/version-page/version-page.component';
|
||||||
import { VersionedItemComponent } from './simple/item-types/versioned-item/versioned-item.component';
|
import { VersionedItemComponent } from './simple/item-types/versioned-item/versioned-item.component';
|
||||||
import { ThemedFileSectionComponent } from './simple/field-components/file-section/themed-file-section.component';
|
import { ThemedFileSectionComponent } from './simple/field-components/file-section/themed-file-section.component';
|
||||||
|
import { OrcidAuthComponent } from './orcid-page/orcid-auth/orcid-auth.component';
|
||||||
|
import { OrcidPageComponent } from './orcid-page/orcid-page.component';
|
||||||
|
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { OrcidSyncSettingsComponent } from './orcid-page/orcid-sync-settings/orcid-sync-settings.component';
|
||||||
|
import { OrcidQueueComponent } from './orcid-page/orcid-queue/orcid-queue.component';
|
||||||
|
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
@@ -67,6 +82,10 @@ const DECLARATIONS = [
|
|||||||
MediaViewerImageComponent,
|
MediaViewerImageComponent,
|
||||||
MiradorViewerComponent,
|
MiradorViewerComponent,
|
||||||
VersionPageComponent,
|
VersionPageComponent,
|
||||||
|
OrcidPageComponent,
|
||||||
|
OrcidAuthComponent,
|
||||||
|
OrcidSyncSettingsComponent,
|
||||||
|
OrcidQueueComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -79,6 +98,7 @@ const DECLARATIONS = [
|
|||||||
JournalEntitiesModule.withEntryComponents(),
|
JournalEntitiesModule.withEntryComponents(),
|
||||||
ResearchEntitiesModule.withEntryComponents(),
|
ResearchEntitiesModule.withEntryComponents(),
|
||||||
NgxGalleryModule,
|
NgxGalleryModule,
|
||||||
|
NgbAccordionModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
...DECLARATIONS,
|
...DECLARATIONS,
|
||||||
|
@@ -0,0 +1,84 @@
|
|||||||
|
<div class="container mb-5">
|
||||||
|
<h2>{{'person.orcid.registry.auth' | translate}}</h2>
|
||||||
|
<ng-container *ngIf="(isLinkedToOrcid() | async); then orcidLinked; else orcidNotLinked"></ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template #orcidLinked>
|
||||||
|
<div data-test="orcidLinked">
|
||||||
|
<div class="row">
|
||||||
|
<div *ngIf="(hasOrcidAuthorizations() | async)" class="col-sm-6 mb-3" data-test="hasOrcidAuthorizations">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header">{{ 'person.page.orcid.granted-authorizations'| translate }}</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="container p-0">
|
||||||
|
<ul>
|
||||||
|
<li *ngFor="let auth of (getOrcidAuthorizations() | async)" data-test="orcidAuthorization">
|
||||||
|
{{getAuthorizationDescription(auth) | translate}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6 mb-3">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header">{{ 'person.page.orcid.missing-authorizations'| translate }}</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="container">
|
||||||
|
<ds-alert *ngIf="!(hasMissingOrcidAuthorizations() | async)" [type]="'alert-success'" data-test="noMissingOrcidAuthorizations">
|
||||||
|
{{'person.page.orcid.no-missing-authorizations-message' | translate}}
|
||||||
|
</ds-alert>
|
||||||
|
<ds-alert *ngIf="(hasMissingOrcidAuthorizations() | async)" [type]="'alert-warning'" data-test="missingOrcidAuthorizations">
|
||||||
|
{{'person.page.orcid.missing-authorizations-message' | translate}}
|
||||||
|
<ul>
|
||||||
|
<li *ngFor="let auth of (getMissingOrcidAuthorizations() | async)" data-test="missingOrcidAuthorization">
|
||||||
|
{{getAuthorizationDescription(auth) | translate }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</ds-alert>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ds-alert *ngIf="(onlyAdminCanDisconnectProfileFromOrcid() | async) && !(ownerCanDisconnectProfileFromOrcid() | async)"
|
||||||
|
[type]="'alert-warning'" data-test="unlinkOnlyAdmin">
|
||||||
|
{{ 'person.page.orcid.remove-orcid-message' | translate}}
|
||||||
|
</ds-alert>
|
||||||
|
<div class="row" *ngIf="(ownerCanDisconnectProfileFromOrcid() | async)" data-test="unlinkOwner">
|
||||||
|
<div class="col">
|
||||||
|
<button type="submit" class="btn btn-danger float-right" (click)="unlinkOrcid()"
|
||||||
|
[disabled]="(unlinkProcessing | async)">
|
||||||
|
<span *ngIf="!(unlinkProcessing | async)"><i
|
||||||
|
class="fas fa-unlink"></i> {{ 'person.page.orcid.unlink' | translate }}</span>
|
||||||
|
<span *ngIf="(unlinkProcessing | async)"><i
|
||||||
|
class='fas fa-circle-notch fa-spin'></i> {{'person.page.orcid.unlink.processing' | translate}}</span>
|
||||||
|
</button>
|
||||||
|
<button *ngIf="(hasMissingOrcidAuthorizations() | async)" type="submit"
|
||||||
|
class="btn btn-primary float-right" (click)="linkOrcid()">
|
||||||
|
<span><i class="fas fa-check"></i> {{ 'person.page.orcid.grant-authorizations' | translate }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #orcidNotLinked>
|
||||||
|
<div data-test="orcidNotLinked">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-2"><img alt="orcid-logo" src="../../../../assets/images/orcid.logo.icon.svg"/></div>
|
||||||
|
<div class="col">
|
||||||
|
<ds-alert [type]="'alert-info'">{{ getOrcidNotLinkedMessage() | async }}</ds-alert>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<button class="btn btn-primary float-right" (click)="linkOrcid()">
|
||||||
|
<i class="fas fa-link"></i>
|
||||||
|
{{'person.page.orcid.link' | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
@@ -0,0 +1,336 @@
|
|||||||
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
|
||||||
|
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
|
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
|
||||||
|
import { OrcidAuthComponent } from './orcid-auth.component';
|
||||||
|
import { NativeWindowService } from '../../../core/services/window.service';
|
||||||
|
import { NativeWindowMockFactory } from '../../../shared/mocks/mock-native-window-ref';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||||
|
import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model';
|
||||||
|
|
||||||
|
describe('OrcidAuthComponent test suite', () => {
|
||||||
|
let comp: OrcidAuthComponent;
|
||||||
|
let fixture: ComponentFixture<OrcidAuthComponent>;
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
let orcidAuthService: jasmine.SpyObj<OrcidAuthService>;
|
||||||
|
let nativeWindowRef;
|
||||||
|
let notificationsService;
|
||||||
|
|
||||||
|
const orcidScopes = [
|
||||||
|
'/authenticate',
|
||||||
|
'/read-limited',
|
||||||
|
'/activities/update',
|
||||||
|
'/person/update'
|
||||||
|
];
|
||||||
|
|
||||||
|
const partialOrcidScopes = [
|
||||||
|
'/authenticate',
|
||||||
|
'/read-limited',
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockItemUnlinkedToOrcid: Item = Object.assign(new Item(), {
|
||||||
|
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [{
|
||||||
|
value: 'test person'
|
||||||
|
}],
|
||||||
|
'dspace.entity.type': [{
|
||||||
|
'value': 'Person'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockItemLinkedToOrcid: Item = Object.assign(new Item(), {
|
||||||
|
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [{
|
||||||
|
value: 'test person'
|
||||||
|
}],
|
||||||
|
'dspace.entity.type': [{
|
||||||
|
'value': 'Person'
|
||||||
|
}],
|
||||||
|
'dspace.object.owner': [{
|
||||||
|
'value': 'test person',
|
||||||
|
'language': null,
|
||||||
|
'authority': 'deced3e7-68e2-495d-bf98-7c44fc33b8ff',
|
||||||
|
'confidence': 600,
|
||||||
|
'place': 0
|
||||||
|
}],
|
||||||
|
'dspace.orcid.authenticated': [{
|
||||||
|
'value': '2022-06-10T15:15:12.952872',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 0
|
||||||
|
}],
|
||||||
|
'dspace.orcid.scope': [{
|
||||||
|
'value': '/authenticate',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 0
|
||||||
|
}, {
|
||||||
|
'value': '/read-limited',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 1
|
||||||
|
}, {
|
||||||
|
'value': '/activities/update',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 2
|
||||||
|
}, {
|
||||||
|
'value': '/person/update',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 3
|
||||||
|
}],
|
||||||
|
'person.identifier.orcid': [{
|
||||||
|
'value': 'orcid-id',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 0
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
orcidAuthService = jasmine.createSpyObj('researcherProfileService', {
|
||||||
|
getOrcidAuthorizationScopes: jasmine.createSpy('getOrcidAuthorizationScopes'),
|
||||||
|
getOrcidAuthorizationScopesByItem: jasmine.createSpy('getOrcidAuthorizationScopesByItem'),
|
||||||
|
getOrcidAuthorizeUrl: jasmine.createSpy('getOrcidAuthorizeUrl'),
|
||||||
|
isLinkedToOrcid: jasmine.createSpy('isLinkedToOrcid'),
|
||||||
|
onlyAdminCanDisconnectProfileFromOrcid: jasmine.createSpy('onlyAdminCanDisconnectProfileFromOrcid'),
|
||||||
|
ownerCanDisconnectProfileFromOrcid: jasmine.createSpy('ownerCanDisconnectProfileFromOrcid'),
|
||||||
|
unlinkOrcidByItem: jasmine.createSpy('unlinkOrcidByItem')
|
||||||
|
});
|
||||||
|
|
||||||
|
void TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
NgbAccordionModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
RouterTestingModule.withRoutes([])
|
||||||
|
],
|
||||||
|
declarations: [OrcidAuthComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
|
||||||
|
{ provide: NotificationsService, useClass: NotificationsServiceStub },
|
||||||
|
{ provide: OrcidAuthService, useValue: orcidAuthService }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(OrcidAuthComponent, {
|
||||||
|
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
fixture = TestBed.createComponent(OrcidAuthComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
orcidAuthService.getOrcidAuthorizationScopes.and.returnValue(of(orcidScopes));
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('when orcid profile is not linked', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
comp.item = mockItemUnlinkedToOrcid;
|
||||||
|
orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([]);
|
||||||
|
orcidAuthService.isLinkedToOrcid.and.returnValue(false);
|
||||||
|
orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
||||||
|
orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||||
|
orcidAuthService.getOrcidAuthorizeUrl.and.returnValue(of('oarcidUrl'));
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should create', fakeAsync(() => {
|
||||||
|
const orcidLinked = fixture.debugElement.query(By.css('[data-test="orcidLinked"]'));
|
||||||
|
const orcidNotLinked = fixture.debugElement.query(By.css('[data-test="orcidNotLinked"]'));
|
||||||
|
expect(orcidLinked).toBeFalsy();
|
||||||
|
expect(orcidNotLinked).toBeTruthy();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should change location on link', () => {
|
||||||
|
nativeWindowRef = (comp as any)._window;
|
||||||
|
scheduler.schedule(() => comp.linkOrcid());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(nativeWindowRef.nativeWindow.location.href).toBe('oarcidUrl');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when orcid profile is linked', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
comp.item = mockItemLinkedToOrcid;
|
||||||
|
orcidAuthService.isLinkedToOrcid.and.returnValue(true);
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('', () => {
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
comp.item = mockItemLinkedToOrcid;
|
||||||
|
notificationsService = (comp as any).notificationsService;
|
||||||
|
orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
|
||||||
|
orcidAuthService.isLinkedToOrcid.and.returnValue(true);
|
||||||
|
orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
||||||
|
orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('and unlink is successfully', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
comp.item = mockItemLinkedToOrcid;
|
||||||
|
orcidAuthService.unlinkOrcidByItem.and.returnValue(createSuccessfulRemoteDataObject$(new ResearcherProfile()));
|
||||||
|
spyOn(comp.unlink, 'emit');
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should show success notification', () => {
|
||||||
|
scheduler.schedule(() => comp.unlinkOrcid());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(notificationsService.success).toHaveBeenCalled();
|
||||||
|
expect(comp.unlink.emit).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and unlink is failed', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
comp.item = mockItemLinkedToOrcid;
|
||||||
|
orcidAuthService.unlinkOrcidByItem.and.returnValue(createFailedRemoteDataObject$());
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should show success notification', () => {
|
||||||
|
scheduler.schedule(() => comp.unlinkOrcid());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and has orcid authorization scopes', () => {
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
comp.item = mockItemLinkedToOrcid;
|
||||||
|
orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
|
||||||
|
orcidAuthService.isLinkedToOrcid.and.returnValue(true);
|
||||||
|
orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
||||||
|
orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should create', fakeAsync(() => {
|
||||||
|
const orcidLinked = fixture.debugElement.query(By.css('[data-test="orcidLinked"]'));
|
||||||
|
const orcidNotLinked = fixture.debugElement.query(By.css('[data-test="orcidNotLinked"]'));
|
||||||
|
expect(orcidLinked).toBeTruthy();
|
||||||
|
expect(orcidNotLinked).toBeFalsy();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should display orcid authorizations', fakeAsync(() => {
|
||||||
|
const orcidAuthorizations = fixture.debugElement.query(By.css('[data-test="hasOrcidAuthorizations"]'));
|
||||||
|
const noMissingOrcidAuthorizations = fixture.debugElement.query(By.css('[data-test="noMissingOrcidAuthorizations"]'));
|
||||||
|
const orcidAuthorizationsList = fixture.debugElement.queryAll(By.css('[data-test="orcidAuthorization"]'));
|
||||||
|
|
||||||
|
expect(orcidAuthorizations).toBeTruthy();
|
||||||
|
expect(noMissingOrcidAuthorizations).toBeTruthy();
|
||||||
|
expect(orcidAuthorizationsList.length).toBe(4);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and has missing orcid authorization scopes', () => {
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
comp.item = mockItemLinkedToOrcid;
|
||||||
|
orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...partialOrcidScopes]);
|
||||||
|
orcidAuthService.isLinkedToOrcid.and.returnValue(true);
|
||||||
|
orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
||||||
|
orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should create', fakeAsync(() => {
|
||||||
|
const orcidLinked = fixture.debugElement.query(By.css('[data-test="orcidLinked"]'));
|
||||||
|
const orcidNotLinked = fixture.debugElement.query(By.css('[data-test="orcidNotLinked"]'));
|
||||||
|
expect(orcidLinked).toBeTruthy();
|
||||||
|
expect(orcidNotLinked).toBeFalsy();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should display orcid authorizations', fakeAsync(() => {
|
||||||
|
const orcidAuthorizations = fixture.debugElement.query(By.css('[data-test="hasOrcidAuthorizations"]'));
|
||||||
|
const missingOrcidAuthorizations = fixture.debugElement.query(By.css('[data-test="missingOrcidAuthorizations"]'));
|
||||||
|
const orcidAuthorizationsList = fixture.debugElement.queryAll(By.css('[data-test="orcidAuthorization"]'));
|
||||||
|
const missingOrcidAuthorizationsList = fixture.debugElement.queryAll(By.css('[data-test="missingOrcidAuthorization"]'));
|
||||||
|
|
||||||
|
expect(orcidAuthorizations).toBeTruthy();
|
||||||
|
expect(missingOrcidAuthorizations).toBeTruthy();
|
||||||
|
expect(orcidAuthorizationsList.length).toBe(2);
|
||||||
|
expect(missingOrcidAuthorizationsList.length).toBe(2);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and only admin can unlink scopes', () => {
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
comp.item = mockItemLinkedToOrcid;
|
||||||
|
orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
|
||||||
|
orcidAuthService.isLinkedToOrcid.and.returnValue(true);
|
||||||
|
orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||||
|
orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should display warning panel', fakeAsync(() => {
|
||||||
|
const unlinkOnlyAdmin = fixture.debugElement.query(By.css('[data-test="unlinkOnlyAdmin"]'));
|
||||||
|
const unlinkOwner = fixture.debugElement.query(By.css('[data-test="unlinkOwner"]'));
|
||||||
|
expect(unlinkOnlyAdmin).toBeTruthy();
|
||||||
|
expect(unlinkOwner).toBeFalsy();
|
||||||
|
}));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and owner can unlink scopes', () => {
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
comp.item = mockItemLinkedToOrcid;
|
||||||
|
orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
|
||||||
|
orcidAuthService.isLinkedToOrcid.and.returnValue(true);
|
||||||
|
orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||||
|
orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should display warning panel', fakeAsync(() => {
|
||||||
|
const unlinkOnlyAdmin = fixture.debugElement.query(By.css('[data-test="unlinkOnlyAdmin"]'));
|
||||||
|
const unlinkOwner = fixture.debugElement.query(By.css('[data-test="unlinkOwner"]'));
|
||||||
|
expect(unlinkOnlyAdmin).toBeFalsy();
|
||||||
|
expect(unlinkOwner).toBeTruthy();
|
||||||
|
}));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
218
src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts
Normal file
218
src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
import { Component, EventEmitter, Inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||||
|
import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-orcid-auth',
|
||||||
|
templateUrl: './orcid-auth.component.html',
|
||||||
|
styleUrls: ['./orcid-auth.component.scss']
|
||||||
|
})
|
||||||
|
export class OrcidAuthComponent implements OnInit, OnChanges {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item for which showing the orcid settings
|
||||||
|
*/
|
||||||
|
@Input() item: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of exposed orcid authorization scopes for the orcid profile
|
||||||
|
*/
|
||||||
|
profileAuthorizationScopes: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of all orcid authorization scopes missing in the orcid profile
|
||||||
|
*/
|
||||||
|
missingAuthorizationScopes: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of all orcid authorization scopes available
|
||||||
|
*/
|
||||||
|
orcidAuthorizationScopes: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean representing if unlink operation is processing
|
||||||
|
*/
|
||||||
|
unlinkProcessing: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean representing if orcid profile is linked
|
||||||
|
*/
|
||||||
|
private isOrcidLinked$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean representing if only admin can disconnect orcid profile
|
||||||
|
*/
|
||||||
|
private onlyAdminCanDisconnectProfileFromOrcid$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean representing if owner can disconnect orcid profile
|
||||||
|
*/
|
||||||
|
private ownerCanDisconnectProfileFromOrcid$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event emitted when orcid profile is unliked successfully
|
||||||
|
*/
|
||||||
|
@Output() unlink: EventEmitter<void> = new EventEmitter<void>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private orcidAuthService: OrcidAuthService,
|
||||||
|
private translateService: TranslateService,
|
||||||
|
private notificationsService: NotificationsService,
|
||||||
|
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.orcidAuthService.getOrcidAuthorizationScopes().subscribe((scopes: string[]) => {
|
||||||
|
this.orcidAuthorizationScopes.next(scopes);
|
||||||
|
this.initOrcidAuthSettings();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (!changes.item.isFirstChange() && changes.item.currentValue !== changes.item.previousValue) {
|
||||||
|
this.initOrcidAuthSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the list of exposed orcid authorization scopes for the orcid profile has values
|
||||||
|
*/
|
||||||
|
hasOrcidAuthorizations(): Observable<boolean> {
|
||||||
|
return this.profileAuthorizationScopes.asObservable().pipe(
|
||||||
|
map((scopes: string[]) => scopes.length > 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the list of exposed orcid authorization scopes for the orcid profile
|
||||||
|
*/
|
||||||
|
getOrcidAuthorizations(): Observable<string[]> {
|
||||||
|
return this.profileAuthorizationScopes.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the list of exposed orcid authorization scopes for the orcid profile has values
|
||||||
|
*/
|
||||||
|
hasMissingOrcidAuthorizations(): Observable<boolean> {
|
||||||
|
return this.missingAuthorizationScopes.asObservable().pipe(
|
||||||
|
map((scopes: string[]) => scopes.length > 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the list of exposed orcid authorization scopes for the orcid profile
|
||||||
|
*/
|
||||||
|
getMissingOrcidAuthorizations(): Observable<string[]> {
|
||||||
|
return this.profileAuthorizationScopes.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a boolean representing if orcid profile is linked
|
||||||
|
*/
|
||||||
|
isLinkedToOrcid(): Observable<boolean> {
|
||||||
|
return this.isOrcidLinked$.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrcidNotLinkedMessage(): Observable<string> {
|
||||||
|
const orcid = this.item.firstMetadataValue('person.identifier.orcid');
|
||||||
|
if (orcid) {
|
||||||
|
return this.translateService.get('person.page.orcid.orcid-not-linked-message', { 'orcid': orcid });
|
||||||
|
} else {
|
||||||
|
return this.translateService.get('person.page.orcid.no-orcid-message');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get label for a given orcid authorization scope
|
||||||
|
*
|
||||||
|
* @param scope
|
||||||
|
*/
|
||||||
|
getAuthorizationDescription(scope: string) {
|
||||||
|
return 'person.page.orcid.scope.' + scope.substring(1).replace('/', '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a boolean representing if only admin can disconnect orcid profile
|
||||||
|
*/
|
||||||
|
onlyAdminCanDisconnectProfileFromOrcid(): Observable<boolean> {
|
||||||
|
return this.onlyAdminCanDisconnectProfileFromOrcid$.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a boolean representing if owner can disconnect orcid profile
|
||||||
|
*/
|
||||||
|
ownerCanDisconnectProfileFromOrcid(): Observable<boolean> {
|
||||||
|
return this.ownerCanDisconnectProfileFromOrcid$.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Link existing person profile with orcid
|
||||||
|
*/
|
||||||
|
linkOrcid(): void {
|
||||||
|
this.orcidAuthService.getOrcidAuthorizeUrl(this.item).subscribe((authorizeUrl) => {
|
||||||
|
this._window.nativeWindow.location.href = authorizeUrl;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlink existing person profile from orcid
|
||||||
|
*/
|
||||||
|
unlinkOrcid(): void {
|
||||||
|
this.unlinkProcessing.next(true);
|
||||||
|
this.orcidAuthService.unlinkOrcidByItem(this.item).pipe(
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
).subscribe((remoteData: RemoteData<ResearcherProfile>) => {
|
||||||
|
this.unlinkProcessing.next(false);
|
||||||
|
if (remoteData.isSuccess) {
|
||||||
|
this.notificationsService.success(this.translateService.get('person.page.orcid.unlink.success'));
|
||||||
|
this.unlink.emit();
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(this.translateService.get('person.page.orcid.unlink.error'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* initialize all Orcid authentication settings
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private initOrcidAuthSettings(): void {
|
||||||
|
|
||||||
|
this.setOrcidAuthorizationsFromItem();
|
||||||
|
|
||||||
|
this.setMissingOrcidAuthorizations();
|
||||||
|
|
||||||
|
this.orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid().subscribe((result) => {
|
||||||
|
this.onlyAdminCanDisconnectProfileFromOrcid$.next(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.orcidAuthService.ownerCanDisconnectProfileFromOrcid().subscribe((result) => {
|
||||||
|
this.ownerCanDisconnectProfileFromOrcid$.next(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.isOrcidLinked$.next(this.orcidAuthService.isLinkedToOrcid(this.item));
|
||||||
|
}
|
||||||
|
|
||||||
|
private setMissingOrcidAuthorizations(): void {
|
||||||
|
const profileScopes = this.orcidAuthService.getOrcidAuthorizationScopesByItem(this.item);
|
||||||
|
const orcidScopes = this.orcidAuthorizationScopes.value;
|
||||||
|
const missingScopes = orcidScopes.filter((scope) => !profileScopes.includes(scope));
|
||||||
|
|
||||||
|
this.missingAuthorizationScopes.next(missingScopes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setOrcidAuthorizationsFromItem(): void {
|
||||||
|
this.profileAuthorizationScopes.next(this.orcidAuthService.getOrcidAuthorizationScopesByItem(this.item));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
19
src/app/item-page/orcid-page/orcid-page.component.html
Normal file
19
src/app/item-page/orcid-page/orcid-page.component.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<div *ngIf="!(processingConnection | async) && (item | async)" class="container">
|
||||||
|
<div class="button-row bottom mb-3">
|
||||||
|
<div class="text-right">
|
||||||
|
<a [routerLink]="getItemPage()" role="button" class="btn btn-outline-secondary" data-test="back-button">
|
||||||
|
<i class="fas fa-arrow-left"></i> {{'item.orcid.return' | translate}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ds-loading *ngIf="(processingConnection | async)" [message]="'person.page.orcid.link.processing' | translate"></ds-loading>
|
||||||
|
<div class="container" *ngIf="!(processingConnection | async) && !(connectionStatus | async)" data-test="error-box">
|
||||||
|
<ds-alert [type]="'alert-danger'">{{'person.page.orcid.link.error.message' | translate}}</ds-alert>
|
||||||
|
</div>
|
||||||
|
<ng-container *ngIf="!(processingConnection | async) && (item | async) && (connectionStatus | async)" >
|
||||||
|
<ds-orcid-auth [item]="(item | async)" (unlink)="updateItem()" data-test="orcid-auth"></ds-orcid-auth>
|
||||||
|
<ds-orcid-sync-setting *ngIf="isLinkedToOrcid()" [item]="(item | async)" (settingsUpdated)="updateItem()" data-test="orcid-sync-setting"></ds-orcid-sync-setting>
|
||||||
|
<ds-orcid-queue *ngIf="isLinkedToOrcid()" [item]="(item | async)"></ds-orcid-queue>
|
||||||
|
</ng-container>
|
220
src/app/item-page/orcid-page/orcid-page.component.spec.ts
Normal file
220
src/app/item-page/orcid-page/orcid-page.component.spec.ts
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
import { NO_ERRORS_SCHEMA, PLATFORM_ID } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
|
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
||||||
|
import { OrcidPageComponent } from './orcid-page.component';
|
||||||
|
import {
|
||||||
|
createFailedRemoteDataObject$,
|
||||||
|
createSuccessfulRemoteDataObject,
|
||||||
|
createSuccessfulRemoteDataObject$
|
||||||
|
} from '../../shared/remote-data.utils';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
|
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||||
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
|
import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model';
|
||||||
|
import { OrcidAuthService } from '../../core/orcid/orcid-auth.service';
|
||||||
|
|
||||||
|
describe('OrcidPageComponent test suite', () => {
|
||||||
|
let comp: OrcidPageComponent;
|
||||||
|
let fixture: ComponentFixture<OrcidPageComponent>;
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
let authService: jasmine.SpyObj<AuthService>;
|
||||||
|
let routeStub: jasmine.SpyObj<ActivatedRouteStub>;
|
||||||
|
let routeData: any;
|
||||||
|
let itemDataService: jasmine.SpyObj<ItemDataService>;
|
||||||
|
let orcidAuthService: jasmine.SpyObj<OrcidAuthService>;
|
||||||
|
|
||||||
|
const mockResearcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), {
|
||||||
|
id: 'test-id',
|
||||||
|
visible: true,
|
||||||
|
type: 'profile',
|
||||||
|
_links: {
|
||||||
|
item: {
|
||||||
|
href: 'https://rest.api/rest/api/profiles/test-id/item'
|
||||||
|
},
|
||||||
|
self: {
|
||||||
|
href: 'https://rest.api/rest/api/profiles/test-id'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const mockItem: Item = Object.assign(new Item(), {
|
||||||
|
id: 'test-id',
|
||||||
|
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'test item'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const mockItemLinkedToOrcid: Item = Object.assign(new Item(), {
|
||||||
|
id: 'test-id',
|
||||||
|
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
value: 'test item'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dspace.orcid.authenticated': [
|
||||||
|
{
|
||||||
|
value: 'true'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
authService = jasmine.createSpyObj('authService', {
|
||||||
|
isAuthenticated: jasmine.createSpy('isAuthenticated'),
|
||||||
|
navigateByUrl: jasmine.createSpy('navigateByUrl')
|
||||||
|
});
|
||||||
|
|
||||||
|
routeData = {
|
||||||
|
dso: createSuccessfulRemoteDataObject(mockItem),
|
||||||
|
};
|
||||||
|
|
||||||
|
routeStub = new ActivatedRouteStub({}, routeData);
|
||||||
|
|
||||||
|
orcidAuthService = jasmine.createSpyObj('OrcidAuthService', {
|
||||||
|
isLinkedToOrcid: jasmine.createSpy('isLinkedToOrcid'),
|
||||||
|
linkOrcidByItem: jasmine.createSpy('linkOrcidByItem'),
|
||||||
|
});
|
||||||
|
|
||||||
|
itemDataService = jasmine.createSpyObj('ItemDataService', {
|
||||||
|
findById: jasmine.createSpy('findById')
|
||||||
|
});
|
||||||
|
|
||||||
|
void TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
RouterTestingModule.withRoutes([])
|
||||||
|
],
|
||||||
|
declarations: [OrcidPageComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: routeStub },
|
||||||
|
{ provide: OrcidAuthService, useValue: orcidAuthService },
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
{ provide: ItemDataService, useValue: itemDataService },
|
||||||
|
{ provide: PLATFORM_ID, useValue: 'browser' },
|
||||||
|
],
|
||||||
|
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
fixture = TestBed.createComponent(OrcidPageComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
authService.isAuthenticated.and.returnValue(observableOf(true));
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('whn has no query param', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
const btn = fixture.debugElement.queryAll(By.css('[data-test="back-button"]'));
|
||||||
|
const auth = fixture.debugElement.query(By.css('[data-test="orcid-auth"]'));
|
||||||
|
const settings = fixture.debugElement.query(By.css('[data-test="orcid-sync-setting"]'));
|
||||||
|
expect(comp).toBeTruthy();
|
||||||
|
expect(btn.length).toBe(1);
|
||||||
|
expect(auth).toBeTruthy();
|
||||||
|
expect(settings).toBeTruthy();
|
||||||
|
expect(comp.itemId).toBe('test-id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call isLinkedToOrcid', () => {
|
||||||
|
comp.isLinkedToOrcid();
|
||||||
|
|
||||||
|
expect(orcidAuthService.isLinkedToOrcid).toHaveBeenCalledWith(comp.item.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update item', fakeAsync(() => {
|
||||||
|
itemDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockItemLinkedToOrcid));
|
||||||
|
scheduler.schedule(() => comp.updateItem());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(comp.item.value).toEqual(mockItemLinkedToOrcid);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when query param contains orcid code', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
spyOn(comp, 'updateItem').and.callThrough();
|
||||||
|
routeStub.testParams = {
|
||||||
|
code: 'orcid-code'
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('and linking to orcid profile is successfully', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
orcidAuthService.linkOrcidByItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile));
|
||||||
|
itemDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockItemLinkedToOrcid));
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should call linkOrcidByItem', () => {
|
||||||
|
expect(orcidAuthService.linkOrcidByItem).toHaveBeenCalledWith(mockItem, 'orcid-code');
|
||||||
|
expect(comp.updateItem).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
const btn = fixture.debugElement.queryAll(By.css('[data-test="back-button"]'));
|
||||||
|
const auth = fixture.debugElement.query(By.css('[data-test="orcid-auth"]'));
|
||||||
|
const settings = fixture.debugElement.query(By.css('[data-test="orcid-sync-setting"]'));
|
||||||
|
expect(comp).toBeTruthy();
|
||||||
|
expect(btn.length).toBe(1);
|
||||||
|
expect(auth).toBeTruthy();
|
||||||
|
expect(settings).toBeTruthy();
|
||||||
|
expect(comp.itemId).toBe('test-id');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and linking to orcid profile is failed', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
orcidAuthService.linkOrcidByItem.and.returnValue(createFailedRemoteDataObject$());
|
||||||
|
itemDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockItemLinkedToOrcid));
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should call linkOrcidByItem', () => {
|
||||||
|
expect(orcidAuthService.linkOrcidByItem).toHaveBeenCalledWith(mockItem, 'orcid-code');
|
||||||
|
expect(comp.updateItem).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
const btn = fixture.debugElement.queryAll(By.css('[data-test="back-button"]'));
|
||||||
|
const auth = fixture.debugElement.query(By.css('[data-test="orcid-auth"]'));
|
||||||
|
const settings = fixture.debugElement.query(By.css('[data-test="orcid-sync-setting"]'));
|
||||||
|
const error = fixture.debugElement.query(By.css('[data-test="error-box"]'));
|
||||||
|
expect(comp).toBeTruthy();
|
||||||
|
expect(btn.length).toBe(1);
|
||||||
|
expect(error).toBeTruthy();
|
||||||
|
expect(auth).toBeFalsy();
|
||||||
|
expect(settings).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
153
src/app/item-page/orcid-page/orcid-page.component.ts
Normal file
153
src/app/item-page/orcid-page/orcid-page.component.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { Component, Inject, OnInit, PLATFORM_ID } from '@angular/core';
|
||||||
|
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||||
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
|
|
||||||
|
import { BehaviorSubject, combineLatest } from 'rxjs';
|
||||||
|
import { map, take } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { OrcidAuthService } from '../../core/orcid/orcid-auth.service';
|
||||||
|
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { getItemPageRoute } from '../item-page-routing-paths';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { redirectOn4xx } from '../../core/shared/authorized.operators';
|
||||||
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component that represents the orcid settings page
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-orcid-page',
|
||||||
|
templateUrl: './orcid-page.component.html',
|
||||||
|
styleUrls: ['./orcid-page.component.scss']
|
||||||
|
})
|
||||||
|
export class OrcidPageComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean representing if the connection operation with orcid profile is in progress
|
||||||
|
*/
|
||||||
|
connectionStatus: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item for which showing the orcid settings
|
||||||
|
*/
|
||||||
|
item: BehaviorSubject<Item> = new BehaviorSubject<Item>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item id for which showing the orcid settings
|
||||||
|
*/
|
||||||
|
itemId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean representing if the connection operation with orcid profile is in progress
|
||||||
|
*/
|
||||||
|
processingConnection: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(PLATFORM_ID) private platformId: any,
|
||||||
|
private authService: AuthService,
|
||||||
|
private itemService: ItemDataService,
|
||||||
|
private orcidAuthService: OrcidAuthService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the item for which showing the orcid settings
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (isPlatformBrowser(this.platformId)) {
|
||||||
|
const codeParam$ = this.route.queryParamMap.pipe(
|
||||||
|
take(1),
|
||||||
|
map((paramMap: ParamMap) => paramMap.get('code')),
|
||||||
|
);
|
||||||
|
|
||||||
|
const item$ = this.route.data.pipe(
|
||||||
|
map((data) => data.dso as RemoteData<Item>),
|
||||||
|
redirectOn4xx(this.router, this.authService),
|
||||||
|
getFirstSucceededRemoteDataPayload()
|
||||||
|
);
|
||||||
|
|
||||||
|
combineLatest([codeParam$, item$]).subscribe(([codeParam, item]) => {
|
||||||
|
this.itemId = item.id;
|
||||||
|
/**
|
||||||
|
* Check if code is present in the query param. If so it means this page is loaded after attempting to
|
||||||
|
* link the person to the ORCID profile, otherwise the person is already linked to ORCID profile
|
||||||
|
*/
|
||||||
|
if (isNotEmpty(codeParam)) {
|
||||||
|
this.linkProfileToOrcid(item, codeParam);
|
||||||
|
} else {
|
||||||
|
this.item.next(item);
|
||||||
|
this.processingConnection.next(false);
|
||||||
|
this.connectionStatus.next(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the current item is linked to an ORCID profile.
|
||||||
|
*
|
||||||
|
* @returns the check result
|
||||||
|
*/
|
||||||
|
isLinkedToOrcid(): boolean {
|
||||||
|
return this.orcidAuthService.isLinkedToOrcid(this.item.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the route to an item's page
|
||||||
|
*/
|
||||||
|
getItemPage(): string {
|
||||||
|
return getItemPageRoute(this.item.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the updated profile item
|
||||||
|
*/
|
||||||
|
updateItem(): void {
|
||||||
|
this.clearRouteParams();
|
||||||
|
this.itemService.findById(this.itemId, false).pipe(
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
).subscribe((itemRD: RemoteData<Item>) => {
|
||||||
|
if (itemRD.hasSucceeded) {
|
||||||
|
this.item.next(itemRD.payload);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Link person item to ORCID profile by using the code received after redirect from ORCID.
|
||||||
|
*
|
||||||
|
* @param person The person item to link to ORCID profile
|
||||||
|
* @param code The auth-code received from ORCID
|
||||||
|
*/
|
||||||
|
private linkProfileToOrcid(person: Item, code: string) {
|
||||||
|
this.orcidAuthService.linkOrcidByItem(person, code).pipe(
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
).subscribe((profileRD: RemoteData<ResearcherProfile>) => {
|
||||||
|
this.processingConnection.next(false);
|
||||||
|
if (profileRD.hasSucceeded) {
|
||||||
|
this.connectionStatus.next(true);
|
||||||
|
this.updateItem();
|
||||||
|
} else {
|
||||||
|
this.item.next(person);
|
||||||
|
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
|
||||||
|
const redirectUrl = this.router.url.split('?')[0];
|
||||||
|
this.router.navigate([redirectUrl]);
|
||||||
|
}
|
||||||
|
}
|
31
src/app/item-page/orcid-page/orcid-page.guard.ts
Normal file
31
src/app/item-page/orcid-page/orcid-page.guard.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { ItemPageResolver } from '../item-page.resolver';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring administrator rights
|
||||||
|
*/
|
||||||
|
export class OrcidPageGuard extends DsoPageSingleFeatureGuard<Item> {
|
||||||
|
constructor(protected resolver: ItemPageResolver,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected authService: AuthService) {
|
||||||
|
super(resolver, authorizationService, router, authService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check administrator authorization rights
|
||||||
|
*/
|
||||||
|
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
|
return observableOf(FeatureID.CanSynchronizeWithORCID);
|
||||||
|
}
|
||||||
|
}
|
@@ -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>
|
@@ -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);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,106 @@
|
|||||||
|
<div class="container mb-5">
|
||||||
|
<h2>{{'person.orcid.sync.setting' | translate}}</h2>
|
||||||
|
<form #f="ngForm" (ngSubmit)="onSubmit(f.form)">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md">
|
||||||
|
<div class="card" data-test="sync-mode">
|
||||||
|
<div class="card-header">{{ 'person.page.orcid.synchronization-mode'| translate }}</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<ds-alert [type]="'alert-info'">
|
||||||
|
{{ 'person.page.orcid.synchronization-mode-message' | translate}}
|
||||||
|
</ds-alert>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="syncMode">{{ 'person.page.orcid.synchronization-mode.label'| translate }}</label>
|
||||||
|
<select class="form-control" [(ngModel)]="currentSyncMode" name="syncMode" id="syncMode"
|
||||||
|
required>
|
||||||
|
<option *ngFor="let syncMode of syncModes"
|
||||||
|
[value]="syncMode.value">{{ syncMode.label | translate }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md mb-3">
|
||||||
|
<div class="card h-100" data-test="sync-mode-publication">
|
||||||
|
<div class="card-header">{{ 'person.page.orcid.publications-preferences'| translate }}</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<ds-alert [type]="'alert-info'">
|
||||||
|
{{ 'person.page.orcid.synchronization-mode-publication-message' | translate}}
|
||||||
|
</ds-alert>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div *ngFor="let option of syncPublicationOptions" class="row form-check">
|
||||||
|
<input type="radio" [(ngModel)]="currentSyncPublications"
|
||||||
|
name="syncPublications" id="publicationOption_{{option.value}}" [value]="option.value"
|
||||||
|
required>
|
||||||
|
<label for="publicationOption_{{option.value}}"
|
||||||
|
class="ml-2">{{option.label | translate}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md mb-3">
|
||||||
|
<div class="card h-100" data-test="sync-mode-funding">
|
||||||
|
<div class="card-header">{{ 'person.page.orcid.funding-preferences'| translate }}</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<ds-alert [type]="'alert-info'">
|
||||||
|
{{ 'person.page.orcid.synchronization-mode-funding-message' | translate}}
|
||||||
|
</ds-alert>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div *ngFor="let option of syncFundingOptions" class="row form-check">
|
||||||
|
<input type="radio" [(ngModel)]="currentSyncFunding"
|
||||||
|
name="syncFundings" id="fundingOption_{{option.value}}" [value]="option.value"
|
||||||
|
required>
|
||||||
|
<label for="fundingOption_{{option.value}}" class="ml-2">{{option.label | translate}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md mb-3">
|
||||||
|
<div class="card h-100" data-test="profile-preferences">
|
||||||
|
<div class="card-header">{{ 'person.page.orcid.profile-preferences'| translate }}</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<ds-alert [type]="'alert-info'">
|
||||||
|
{{ 'person.page.orcid.synchronization-mode-profile-message' | translate}}
|
||||||
|
</ds-alert>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div *ngFor="let option of syncProfileOptions" class="row form-check">
|
||||||
|
<input type="checkbox" [(ngModel)]="option.checked"
|
||||||
|
name="syncProfile_{{option.value}}" id="profileOption_{{option.value}}"
|
||||||
|
[value]="option.value">
|
||||||
|
<label for="profileOption_{{option.value}}" class="ml-2">{{option.label | translate}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<button type="submit" class="btn btn-primary float-right">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
{{ 'person.page.orcid.save.preference.changes' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
@@ -0,0 +1,261 @@
|
|||||||
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { Operation } from 'fast-json-patch';
|
||||||
|
|
||||||
|
import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service';
|
||||||
|
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
|
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||||
|
import { OrcidSyncSettingsComponent } from './orcid-sync-settings.component';
|
||||||
|
import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model';
|
||||||
|
|
||||||
|
describe('OrcidAuthComponent test suite', () => {
|
||||||
|
let comp: OrcidSyncSettingsComponent;
|
||||||
|
let fixture: ComponentFixture<OrcidSyncSettingsComponent>;
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
let researcherProfileService: jasmine.SpyObj<ResearcherProfileService>;
|
||||||
|
let notificationsService;
|
||||||
|
let formGroup: FormGroup;
|
||||||
|
|
||||||
|
const mockResearcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), {
|
||||||
|
id: 'test-id',
|
||||||
|
visible: true,
|
||||||
|
type: 'profile',
|
||||||
|
_links: {
|
||||||
|
item: {
|
||||||
|
href: 'https://rest.api/rest/api/profiles/test-id/item'
|
||||||
|
},
|
||||||
|
self: {
|
||||||
|
href: 'https://rest.api/rest/api/profiles/test-id'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const 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
|
||||||
|
}],
|
||||||
|
'dspace.orcid.sync-mode': [{
|
||||||
|
'value': 'MANUAL',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 0
|
||||||
|
}],
|
||||||
|
'dspace.orcid.sync-profile': [{
|
||||||
|
'value': 'BIOGRAPHICAL',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 0
|
||||||
|
}, {
|
||||||
|
'value': 'IDENTIFIERS',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 1
|
||||||
|
}],
|
||||||
|
'dspace.orcid.sync-publications': [{
|
||||||
|
'value': 'ALL',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 0
|
||||||
|
}],
|
||||||
|
'person.identifier.orcid': [{
|
||||||
|
'value': 'orcid-id',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 0
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
researcherProfileService = jasmine.createSpyObj('researcherProfileService', {
|
||||||
|
findByRelatedItem: jasmine.createSpy('findByRelatedItem'),
|
||||||
|
updateByOrcidOperations: jasmine.createSpy('updateByOrcidOperations')
|
||||||
|
});
|
||||||
|
|
||||||
|
void TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
NgbAccordionModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
RouterTestingModule.withRoutes([])
|
||||||
|
],
|
||||||
|
declarations: [OrcidSyncSettingsComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: NotificationsService, useClass: NotificationsServiceStub },
|
||||||
|
{ provide: ResearcherProfileService, useValue: researcherProfileService }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(OrcidSyncSettingsComponent, {
|
||||||
|
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
fixture = TestBed.createComponent(OrcidSyncSettingsComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
comp.item = mockItemLinkedToOrcid;
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should create cards properly', () => {
|
||||||
|
const modes = fixture.debugElement.query(By.css('[data-test="sync-mode"]'));
|
||||||
|
const publication = fixture.debugElement.query(By.css('[data-test="sync-mode-publication"]'));
|
||||||
|
const funding = fixture.debugElement.query(By.css('[data-test="sync-mode-funding"]'));
|
||||||
|
const preferences = fixture.debugElement.query(By.css('[data-test="profile-preferences"]'));
|
||||||
|
expect(modes).toBeTruthy();
|
||||||
|
expect(publication).toBeTruthy();
|
||||||
|
expect(funding).toBeTruthy();
|
||||||
|
expect(preferences).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should init sync modes properly', () => {
|
||||||
|
expect(comp.currentSyncMode).toBe('MANUAL');
|
||||||
|
expect(comp.currentSyncPublications).toBe('ALL');
|
||||||
|
expect(comp.currentSyncFunding).toBe('DISABLED');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('form submit', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
notificationsService = (comp as any).notificationsService;
|
||||||
|
formGroup = new FormGroup({
|
||||||
|
syncMode: new FormControl('MANUAL'),
|
||||||
|
syncFundings: new FormControl('ALL'),
|
||||||
|
syncPublications: new FormControl('ALL'),
|
||||||
|
syncProfile_BIOGRAPHICAL: new FormControl(true),
|
||||||
|
syncProfile_IDENTIFIERS: new FormControl(true),
|
||||||
|
});
|
||||||
|
spyOn(comp.settingsUpdated, 'emit');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call updateByOrcidOperations properly', () => {
|
||||||
|
researcherProfileService.findByRelatedItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile));
|
||||||
|
researcherProfileService.updateByOrcidOperations.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile));
|
||||||
|
const expectedOps: Operation[] = [
|
||||||
|
{
|
||||||
|
path: '/orcid/mode',
|
||||||
|
op: 'replace',
|
||||||
|
value: 'MANUAL'
|
||||||
|
}, {
|
||||||
|
path: '/orcid/publications',
|
||||||
|
op: 'replace',
|
||||||
|
value: 'ALL'
|
||||||
|
}, {
|
||||||
|
path: '/orcid/fundings',
|
||||||
|
op: 'replace',
|
||||||
|
value: 'ALL'
|
||||||
|
}, {
|
||||||
|
path: '/orcid/profile',
|
||||||
|
op: 'replace',
|
||||||
|
value: 'BIOGRAPHICAL,IDENTIFIERS'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
scheduler.schedule(() => comp.onSubmit(formGroup));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(researcherProfileService.updateByOrcidOperations).toHaveBeenCalledWith(mockResearcherProfile, expectedOps);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show notification on success', () => {
|
||||||
|
researcherProfileService.findByRelatedItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile));
|
||||||
|
researcherProfileService.updateByOrcidOperations.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile));
|
||||||
|
|
||||||
|
scheduler.schedule(() => comp.onSubmit(formGroup));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(notificationsService.success).toHaveBeenCalled();
|
||||||
|
expect(comp.settingsUpdated.emit).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show notification on error', () => {
|
||||||
|
researcherProfileService.findByRelatedItem.and.returnValue(createFailedRemoteDataObject$());
|
||||||
|
|
||||||
|
scheduler.schedule(() => comp.onSubmit(formGroup));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
|
expect(comp.settingsUpdated.emit).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show notification on error', () => {
|
||||||
|
researcherProfileService.findByRelatedItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile));
|
||||||
|
researcherProfileService.updateByOrcidOperations.and.returnValue(createFailedRemoteDataObject$());
|
||||||
|
|
||||||
|
scheduler.schedule(() => comp.onSubmit(formGroup));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
|
expect(comp.settingsUpdated.emit).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,196 @@
|
|||||||
|
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||||
|
import { FormGroup } from '@angular/forms';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { Operation } from 'fast-json-patch';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { switchMap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-orcid-sync-setting',
|
||||||
|
templateUrl: './orcid-sync-settings.component.html',
|
||||||
|
styleUrls: ['./orcid-sync-settings.component.scss']
|
||||||
|
})
|
||||||
|
export class OrcidSyncSettingsComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item for which showing the orcid settings
|
||||||
|
*/
|
||||||
|
@Input() item: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The prefix used for i18n keys
|
||||||
|
*/
|
||||||
|
messagePrefix = 'person.page.orcid';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current synchronization mode
|
||||||
|
*/
|
||||||
|
currentSyncMode: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current synchronization mode for publications
|
||||||
|
*/
|
||||||
|
currentSyncPublications: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current synchronization mode for funding
|
||||||
|
*/
|
||||||
|
currentSyncFunding: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The synchronization options
|
||||||
|
*/
|
||||||
|
syncModes: { value: string, label: string }[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The synchronization options for publications
|
||||||
|
*/
|
||||||
|
syncPublicationOptions: { value: string, label: string }[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The synchronization options for funding
|
||||||
|
*/
|
||||||
|
syncFundingOptions: { value: string, label: string }[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The profile synchronization options
|
||||||
|
*/
|
||||||
|
syncProfileOptions: { value: string, label: string, checked: boolean }[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event emitted when settings are updated
|
||||||
|
*/
|
||||||
|
@Output() settingsUpdated: EventEmitter<void> = new EventEmitter<void>();
|
||||||
|
|
||||||
|
constructor(private researcherProfileService: ResearcherProfileService,
|
||||||
|
private notificationsService: NotificationsService,
|
||||||
|
private translateService: TranslateService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init orcid settings form
|
||||||
|
*/
|
||||||
|
ngOnInit() {
|
||||||
|
this.syncModes = [
|
||||||
|
{
|
||||||
|
label: this.messagePrefix + '.synchronization-mode.batch',
|
||||||
|
value: 'BATCH'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.messagePrefix + '.synchronization-mode.manual',
|
||||||
|
value: 'MANUAL'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
this.syncPublicationOptions = ['DISABLED', 'ALL']
|
||||||
|
.map((value) => {
|
||||||
|
return {
|
||||||
|
label: this.messagePrefix + '.sync-publications.' + value.toLowerCase(),
|
||||||
|
value: value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.syncFundingOptions = ['DISABLED', 'ALL']
|
||||||
|
.map((value) => {
|
||||||
|
return {
|
||||||
|
label: this.messagePrefix + '.sync-fundings.' + value.toLowerCase(),
|
||||||
|
value: value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const syncProfilePreferences = this.item.allMetadataValues('dspace.orcid.sync-profile');
|
||||||
|
|
||||||
|
this.syncProfileOptions = ['BIOGRAPHICAL', 'IDENTIFIERS']
|
||||||
|
.map((value) => {
|
||||||
|
return {
|
||||||
|
label: this.messagePrefix + '.sync-profile.' + value.toLowerCase(),
|
||||||
|
value: value,
|
||||||
|
checked: syncProfilePreferences.includes(value)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.currentSyncMode = this.getCurrentPreference('dspace.orcid.sync-mode', ['BATCH', 'MANUAL'], 'MANUAL');
|
||||||
|
this.currentSyncPublications = this.getCurrentPreference('dspace.orcid.sync-publications', ['DISABLED', 'ALL'], 'DISABLED');
|
||||||
|
this.currentSyncFunding = this.getCurrentPreference('dspace.orcid.sync-fundings', ['DISABLED', 'ALL'], 'DISABLED');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate path operations to save orcid synchronization preferences
|
||||||
|
*
|
||||||
|
* @param form The form group
|
||||||
|
*/
|
||||||
|
onSubmit(form: FormGroup): void {
|
||||||
|
const operations: Operation[] = [];
|
||||||
|
this.fillOperationsFor(operations, '/orcid/mode', form.value.syncMode);
|
||||||
|
this.fillOperationsFor(operations, '/orcid/publications', form.value.syncPublications);
|
||||||
|
this.fillOperationsFor(operations, '/orcid/fundings', form.value.syncFundings);
|
||||||
|
|
||||||
|
const syncProfileValue = this.syncProfileOptions
|
||||||
|
.map((syncProfileOption => syncProfileOption.value))
|
||||||
|
.filter((value) => form.value['syncProfile_' + value])
|
||||||
|
.join(',');
|
||||||
|
|
||||||
|
this.fillOperationsFor(operations, '/orcid/profile', syncProfileValue);
|
||||||
|
|
||||||
|
if (operations.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.researcherProfileService.findByRelatedItem(this.item).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
switchMap((profileRD: RemoteData<ResearcherProfile>) => {
|
||||||
|
if (profileRD.hasSucceeded) {
|
||||||
|
return this.researcherProfileService.updateByOrcidOperations(profileRD.payload, operations).pipe(
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return of(profileRD);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
).subscribe((remoteData: RemoteData<ResearcherProfile>) => {
|
||||||
|
if (remoteData.isSuccess) {
|
||||||
|
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.success'));
|
||||||
|
this.settingsUpdated.emit();
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.error'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve setting saved in the item's metadata
|
||||||
|
*
|
||||||
|
* @param metadataField The metadata name that contains setting
|
||||||
|
* @param allowedValues The allowed values
|
||||||
|
* @param defaultValue The default value
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private getCurrentPreference(metadataField: string, allowedValues: string[], defaultValue: string): string {
|
||||||
|
const currentPreference = this.item.firstMetadataValue(metadataField);
|
||||||
|
return (currentPreference && allowedValues.includes(currentPreference)) ? currentPreference : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a replace patch operation
|
||||||
|
*
|
||||||
|
* @param operations
|
||||||
|
* @param path
|
||||||
|
* @param currentValue
|
||||||
|
*/
|
||||||
|
private fillOperationsFor(operations: Operation[], path: string, currentValue: string): void {
|
||||||
|
operations.push({
|
||||||
|
path: path,
|
||||||
|
op: 'replace',
|
||||||
|
value: currentValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -12,6 +12,9 @@
|
|||||||
{{'publication.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
{{'publication.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="pl-2 space-children-mr">
|
<div class="pl-2 space-children-mr">
|
||||||
|
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
|
||||||
|
[tooltipMsgCreate]="'item.page.version.create'"
|
||||||
|
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
|
||||||
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'publication.page.edit'"></ds-dso-page-edit-button>
|
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'publication.page.edit'"></ds-dso-page-edit-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
@@ -17,7 +17,7 @@ import { RemoteData } from '../../../../core/data/remote-data';
|
|||||||
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
||||||
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
|
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import { MetadataMap } from '../../../../core/shared/metadata.models';
|
import { MetadataMap } from '../../../../core/shared/metadata.models';
|
||||||
import { UUIDService } from '../../../../core/shared/uuid.service';
|
import { UUIDService } from '../../../../core/shared/uuid.service';
|
||||||
import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock';
|
import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock';
|
||||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||||
@@ -26,22 +26,16 @@ import { TruncatableService } from '../../../../shared/truncatable/truncatable.s
|
|||||||
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
|
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
|
||||||
import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component';
|
import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component';
|
||||||
import {
|
import {
|
||||||
createRelationshipsObservable,
|
createRelationshipsObservable, getIIIFEnabled, getIIIFSearchEnabled, mockRouteService
|
||||||
iiifEnabled,
|
|
||||||
iiifSearchEnabled, mockRouteService
|
|
||||||
} from '../shared/item.component.spec';
|
} from '../shared/item.component.spec';
|
||||||
import { PublicationComponent } from './publication.component';
|
import { PublicationComponent } from './publication.component';
|
||||||
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
||||||
import { RouteService } from '../../../../core/services/route.service';
|
import { RouteService } from '../../../../core/services/route.service';
|
||||||
|
import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service';
|
||||||
const iiifEnabledMap: MetadataMap = {
|
import { VersionDataService } from '../../../../core/data/version-data.service';
|
||||||
'dspace.iiif.enabled': [iiifEnabled],
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
};
|
import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service';
|
||||||
|
import { SearchService } from '../../../../core/shared/search/search.service';
|
||||||
const iiifEnabledWithSearchMap: MetadataMap = {
|
|
||||||
'dspace.iiif.enabled': [iiifEnabled],
|
|
||||||
'iiif.search.enabled': [iiifSearchEnabled],
|
|
||||||
};
|
|
||||||
|
|
||||||
const noMetadata = new MetadataMap();
|
const noMetadata = new MetadataMap();
|
||||||
|
|
||||||
@@ -64,12 +58,15 @@ describe('PublicationComponent', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot({
|
imports: [
|
||||||
loader: {
|
TranslateModule.forRoot({
|
||||||
provide: TranslateLoader,
|
loader: {
|
||||||
useClass: TranslateLoaderMock
|
provide: TranslateLoader,
|
||||||
}
|
useClass: TranslateLoaderMock
|
||||||
})],
|
}
|
||||||
|
}),
|
||||||
|
RouterTestingModule,
|
||||||
|
],
|
||||||
declarations: [PublicationComponent, GenericItemPageFieldComponent, TruncatePipe],
|
declarations: [PublicationComponent, GenericItemPageFieldComponent, TruncatePipe],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ItemDataService, useValue: {} },
|
{ provide: ItemDataService, useValue: {} },
|
||||||
@@ -85,18 +82,23 @@ describe('PublicationComponent', () => {
|
|||||||
{ provide: HttpClient, useValue: {} },
|
{ provide: HttpClient, useValue: {} },
|
||||||
{ provide: DSOChangeAnalyzer, useValue: {} },
|
{ provide: DSOChangeAnalyzer, useValue: {} },
|
||||||
{ provide: DefaultChangeAnalyzer, useValue: {} },
|
{ provide: DefaultChangeAnalyzer, useValue: {} },
|
||||||
|
{ provide: VersionHistoryDataService, useValue: {} },
|
||||||
|
{ provide: VersionDataService, useValue: {} },
|
||||||
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
||||||
|
{ provide: WorkspaceitemDataService, useValue: {} },
|
||||||
|
{ provide: SearchService, useValue: {} },
|
||||||
{ provide: RouteService, useValue: mockRouteService }
|
{ provide: RouteService, useValue: mockRouteService }
|
||||||
],
|
],
|
||||||
|
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(PublicationComponent, {
|
}).overrideComponent(PublicationComponent, {
|
||||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||||
}).compileComponents();
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('default view', () => {
|
describe('default view', () => {
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
TestBed.compileComponents();
|
||||||
fixture = TestBed.createComponent(PublicationComponent);
|
fixture = TestBed.createComponent(PublicationComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
comp.object = getItem(noMetadata);
|
comp.object = getItem(noMetadata);
|
||||||
@@ -137,6 +139,42 @@ describe('PublicationComponent', () => {
|
|||||||
describe('with IIIF viewer', () => {
|
describe('with IIIF viewer', () => {
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
const iiifEnabledMap: MetadataMap = {
|
||||||
|
'dspace.iiif.enabled': [getIIIFEnabled(true)],
|
||||||
|
'iiif.search.enabled': [getIIIFSearchEnabled(false)],
|
||||||
|
};
|
||||||
|
TestBed.compileComponents();
|
||||||
|
fixture = TestBed.createComponent(PublicationComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
comp.object = getItem(iiifEnabledMap);
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should contain an iiif viewer component', () => {
|
||||||
|
const fields = fixture.debugElement.queryAll(By.css('ds-mirador-viewer'));
|
||||||
|
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
it('should not retrieve the query term for previous route', fakeAsync((): void => {
|
||||||
|
//tick(10)
|
||||||
|
expect(comp.iiifQuery$).toBeFalsy();
|
||||||
|
}));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with IIIF viewer and search', () => {
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
const localMockRouteService = {
|
||||||
|
getPreviousUrl(): Observable<string> {
|
||||||
|
return of('/search?query=test%20query&fakeParam=true');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const iiifEnabledMap: MetadataMap = {
|
||||||
|
'dspace.iiif.enabled': [getIIIFEnabled(true)],
|
||||||
|
'iiif.search.enabled': [getIIIFSearchEnabled(true)],
|
||||||
|
};
|
||||||
|
TestBed.overrideProvider(RouteService, {useValue: localMockRouteService});
|
||||||
|
TestBed.compileComponents();
|
||||||
fixture = TestBed.createComponent(PublicationComponent);
|
fixture = TestBed.createComponent(PublicationComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
comp.object = getItem(iiifEnabledMap);
|
comp.object = getItem(iiifEnabledMap);
|
||||||
@@ -148,15 +186,29 @@ describe('PublicationComponent', () => {
|
|||||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should retrieve the query term for previous route', fakeAsync((): void => {
|
||||||
|
expect(comp.iiifQuery$.subscribe(result => expect(result).toEqual('test query')));
|
||||||
|
}));
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with IIIF viewer and search', () => {
|
describe('with IIIF viewer and search but no previous search query', () => {
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
mockRouteService.getPreviousUrl.and.returnValue(of(['/search?q=bird&motivation=painting','/item']));
|
const localMockRouteService = {
|
||||||
|
getPreviousUrl(): Observable<string> {
|
||||||
|
return of('/item');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const iiifEnabledMap: MetadataMap = {
|
||||||
|
'dspace.iiif.enabled': [getIIIFEnabled(true)],
|
||||||
|
'iiif.search.enabled': [getIIIFSearchEnabled(true)],
|
||||||
|
};
|
||||||
|
TestBed.overrideProvider(RouteService, {useValue: localMockRouteService});
|
||||||
|
TestBed.compileComponents();
|
||||||
fixture = TestBed.createComponent(PublicationComponent);
|
fixture = TestBed.createComponent(PublicationComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
comp.object = getItem(iiifEnabledWithSearchMap);
|
comp.object = getItem(iiifEnabledMap);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -165,11 +217,12 @@ describe('PublicationComponent', () => {
|
|||||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call the RouteService getHistory method', () => {
|
it('should not retrieve the query term for previous route', fakeAsync( () => {
|
||||||
expect(mockRouteService.getPreviousUrl).toHaveBeenCalled();
|
let emitted;
|
||||||
});
|
comp.iiifQuery$.subscribe(result => emitted = result);
|
||||||
|
tick(10);
|
||||||
|
expect(emitted).toBeUndefined();
|
||||||
|
}));
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||||
import { ItemComponent } from '../shared/item.component';
|
|
||||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
|
import { VersionedItemComponent } from '../versioned-item/versioned-item.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that represents a publication Item page
|
* Component that represents a publication Item page
|
||||||
@@ -14,6 +14,6 @@ import { listableObjectComponent } from '../../../../shared/object-collection/sh
|
|||||||
templateUrl: './publication.component.html',
|
templateUrl: './publication.component.html',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class PublicationComponent extends ItemComponent {
|
export class PublicationComponent extends VersionedItemComponent {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { filter, map } from 'rxjs/operators';
|
import { filter, map, take } from 'rxjs/operators';
|
||||||
import { RouteService } from '../../../../core/services/route.service';
|
import { RouteService } from '../../../../core/services/route.service';
|
||||||
|
import { DefaultUrlSerializer, UrlTree } from '@angular/router';
|
||||||
|
|
||||||
export const isIiifEnabled = (item: Item) => {
|
export const isIiifEnabled = (item: Item) => {
|
||||||
return !!item.firstMetadataValue('dspace.iiif.enabled');
|
return !!item.firstMetadataValue('dspace.iiif.enabled');
|
||||||
@@ -21,15 +22,15 @@ export const isIiifSearchEnabled = (item: Item) => {
|
|||||||
* @param routeService
|
* @param routeService
|
||||||
*/
|
*/
|
||||||
export const getDSpaceQuery = (item: Item, routeService: RouteService): Observable<string> => {
|
export const getDSpaceQuery = (item: Item, routeService: RouteService): Observable<string> => {
|
||||||
|
|
||||||
return routeService.getPreviousUrl().pipe(
|
return routeService.getPreviousUrl().pipe(
|
||||||
filter(r => {
|
filter(r => {
|
||||||
return r.includes('/search');
|
return r.includes('/search');
|
||||||
}),
|
}),
|
||||||
map(r => {
|
map((r: string) => {
|
||||||
const arr = r.split('&');
|
const url: UrlTree = new DefaultUrlSerializer().parse(r);
|
||||||
const q = arr[1];
|
return url.queryParamMap.get('query');
|
||||||
const v = q.split('=');
|
}),
|
||||||
return v[1];
|
take(1)
|
||||||
})
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -4,7 +4,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
import {Observable, of as observableOf} from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
|
||||||
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
||||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
||||||
@@ -32,24 +32,33 @@ import { ItemComponent } from './item.component';
|
|||||||
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
||||||
import { RouteService } from '../../../../core/services/route.service';
|
import { RouteService } from '../../../../core/services/route.service';
|
||||||
import { MetadataValue } from '../../../../core/shared/metadata.models';
|
import { MetadataValue } from '../../../../core/shared/metadata.models';
|
||||||
import {AuthorizationDataService} from '../../../../core/data/feature-authorization/authorization-data.service';
|
import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service';
|
||||||
import {ResearcherProfileService} from '../../../../core/profile/researcher-profile.service';
|
import { SearchService } from '../../../../core/shared/search/search.service';
|
||||||
|
import { VersionDataService } from '../../../../core/data/version-data.service';
|
||||||
|
import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { ResearcherProfileService } from '../../../../core/profile/researcher-profile.service';
|
||||||
|
|
||||||
export const iiifEnabled = Object.assign(new MetadataValue(),{
|
export function getIIIFSearchEnabled(enabled: boolean): MetadataValue {
|
||||||
'value': 'true',
|
return Object.assign(new MetadataValue(), {
|
||||||
'language': null,
|
'value': enabled,
|
||||||
'authority': null,
|
'language': null,
|
||||||
'confidence': -1,
|
'authority': null,
|
||||||
'place': 0
|
'confidence': -1,
|
||||||
});
|
'place': 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const iiifSearchEnabled = Object.assign(new MetadataValue(), {
|
export function getIIIFEnabled(enabled: boolean): MetadataValue {
|
||||||
'value': 'true',
|
return Object.assign(new MetadataValue(), {
|
||||||
'language': null,
|
'value': enabled,
|
||||||
'authority': null,
|
'language': null,
|
||||||
'confidence': -1,
|
'authority': null,
|
||||||
'place': 0
|
'confidence': -1,
|
||||||
});
|
'place': 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const mockRouteService = jasmine.createSpyObj('RouteService', ['getPreviousUrl']);
|
export const mockRouteService = jasmine.createSpyObj('RouteService', ['getPreviousUrl']);
|
||||||
|
|
||||||
@@ -77,12 +86,15 @@ export function getItemPageFieldsTest(mockItem: Item, component) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot({
|
imports: [
|
||||||
loader: {
|
TranslateModule.forRoot({
|
||||||
provide: TranslateLoader,
|
loader: {
|
||||||
useClass: TranslateLoaderMock
|
provide: TranslateLoader,
|
||||||
}
|
useClass: TranslateLoaderMock
|
||||||
})],
|
}
|
||||||
|
}),
|
||||||
|
RouterTestingModule,
|
||||||
|
],
|
||||||
declarations: [component, GenericItemPageFieldComponent, TruncatePipe],
|
declarations: [component, GenericItemPageFieldComponent, TruncatePipe],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ItemDataService, useValue: {} },
|
{ provide: ItemDataService, useValue: {} },
|
||||||
@@ -96,9 +108,13 @@ export function getItemPageFieldsTest(mockItem: Item, component) {
|
|||||||
{ provide: HALEndpointService, useValue: {} },
|
{ provide: HALEndpointService, useValue: {} },
|
||||||
{ provide: HttpClient, useValue: {} },
|
{ provide: HttpClient, useValue: {} },
|
||||||
{ provide: DSOChangeAnalyzer, useValue: {} },
|
{ provide: DSOChangeAnalyzer, useValue: {} },
|
||||||
|
{ provide: VersionHistoryDataService, useValue: {} },
|
||||||
|
{ provide: VersionDataService, useValue: {} },
|
||||||
{ provide: NotificationsService, useValue: {} },
|
{ provide: NotificationsService, useValue: {} },
|
||||||
{ provide: DefaultChangeAnalyzer, useValue: {} },
|
{ provide: DefaultChangeAnalyzer, useValue: {} },
|
||||||
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
||||||
|
{ provide: WorkspaceitemDataService, useValue: {} },
|
||||||
|
{ provide: SearchService, useValue: {} },
|
||||||
{ provide: RouteService, useValue: {} },
|
{ provide: RouteService, useValue: {} },
|
||||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
{ provide: ResearcherProfileService, useValue: {} }
|
{ provide: ResearcherProfileService, useValue: {} }
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
|
||||||
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
||||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
||||||
@@ -26,13 +26,10 @@ import { TruncatableService } from '../../../../shared/truncatable/truncatable.s
|
|||||||
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
|
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
|
||||||
import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component';
|
import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component';
|
||||||
import {
|
import {
|
||||||
createRelationshipsObservable,
|
createRelationshipsObservable, getIIIFEnabled, getIIIFSearchEnabled, mockRouteService
|
||||||
iiifEnabled,
|
|
||||||
iiifSearchEnabled, mockRouteService
|
|
||||||
} from '../shared/item.component.spec';
|
} from '../shared/item.component.spec';
|
||||||
import { UntypedItemComponent } from './untyped-item.component';
|
import { UntypedItemComponent } from './untyped-item.component';
|
||||||
import { RouteService } from '../../../../core/services/route.service';
|
import { RouteService } from '../../../../core/services/route.service';
|
||||||
import { of } from 'rxjs';
|
|
||||||
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
||||||
import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service';
|
import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service';
|
||||||
import { VersionDataService } from '../../../../core/data/version-data.service';
|
import { VersionDataService } from '../../../../core/data/version-data.service';
|
||||||
@@ -41,16 +38,6 @@ import { WorkspaceitemDataService } from '../../../../core/submission/workspacei
|
|||||||
import { SearchService } from '../../../../core/shared/search/search.service';
|
import { SearchService } from '../../../../core/shared/search/search.service';
|
||||||
import { ItemVersionsSharedService } from '../../../../shared/item/item-versions/item-versions-shared.service';
|
import { ItemVersionsSharedService } from '../../../../shared/item/item-versions/item-versions-shared.service';
|
||||||
|
|
||||||
|
|
||||||
const iiifEnabledMap: MetadataMap = {
|
|
||||||
'dspace.iiif.enabled': [iiifEnabled],
|
|
||||||
};
|
|
||||||
|
|
||||||
const iiifEnabledWithSearchMap: MetadataMap = {
|
|
||||||
'dspace.iiif.enabled': [iiifEnabled],
|
|
||||||
'iiif.search.enabled': [iiifSearchEnabled],
|
|
||||||
};
|
|
||||||
|
|
||||||
const noMetadata = new MetadataMap();
|
const noMetadata = new MetadataMap();
|
||||||
|
|
||||||
function getItem(metadata: MetadataMap) {
|
function getItem(metadata: MetadataMap) {
|
||||||
@@ -108,11 +95,12 @@ describe('UntypedItemComponent', () => {
|
|||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(UntypedItemComponent, {
|
}).overrideComponent(UntypedItemComponent, {
|
||||||
set: {changeDetection: ChangeDetectionStrategy.Default}
|
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||||
}).compileComponents();
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('default view', () => {
|
describe('default view', () => {
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
TestBed.compileComponents();
|
||||||
fixture = TestBed.createComponent(UntypedItemComponent);
|
fixture = TestBed.createComponent(UntypedItemComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
comp.object = getItem(noMetadata);
|
comp.object = getItem(noMetadata);
|
||||||
@@ -159,6 +147,41 @@ describe('UntypedItemComponent', () => {
|
|||||||
describe('with IIIF viewer', () => {
|
describe('with IIIF viewer', () => {
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
const iiifEnabledMap: MetadataMap = {
|
||||||
|
'dspace.iiif.enabled': [getIIIFEnabled(true)],
|
||||||
|
'iiif.search.enabled': [getIIIFSearchEnabled(false)],
|
||||||
|
};
|
||||||
|
TestBed.compileComponents();
|
||||||
|
fixture = TestBed.createComponent(UntypedItemComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
comp.object = getItem(iiifEnabledMap);
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should contain an iiif viewer component', () => {
|
||||||
|
const fields = fixture.debugElement.queryAll(By.css('ds-mirador-viewer'));
|
||||||
|
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
it('should not retrieve the query term for previous route', (): void => {
|
||||||
|
expect(comp.iiifQuery$).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with IIIF viewer and search', () => {
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
const localMockRouteService = {
|
||||||
|
getPreviousUrl(): Observable<string> {
|
||||||
|
return of('/search?query=test%20query&fakeParam=true');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const iiifEnabledMap: MetadataMap = {
|
||||||
|
'dspace.iiif.enabled': [getIIIFEnabled(true)],
|
||||||
|
'iiif.search.enabled': [getIIIFSearchEnabled(true)],
|
||||||
|
};
|
||||||
|
TestBed.overrideProvider(RouteService, {useValue: localMockRouteService});
|
||||||
|
TestBed.compileComponents();
|
||||||
fixture = TestBed.createComponent(UntypedItemComponent);
|
fixture = TestBed.createComponent(UntypedItemComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
comp.object = getItem(iiifEnabledMap);
|
comp.object = getItem(iiifEnabledMap);
|
||||||
@@ -170,15 +193,29 @@ describe('UntypedItemComponent', () => {
|
|||||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should retrieve the query term for previous route', (): void => {
|
||||||
|
expect(comp.iiifQuery$.subscribe(result => expect(result).toEqual('test query')));
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with IIIF viewer and search', () => {
|
describe('with IIIF viewer and search but no previous search query', () => {
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
mockRouteService.getPreviousUrl.and.returnValue(of(['/search?q=bird&motivation=painting','/item']));
|
const localMockRouteService = {
|
||||||
|
getPreviousUrl(): Observable<string> {
|
||||||
|
return of('/item');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const iiifEnabledMap: MetadataMap = {
|
||||||
|
'dspace.iiif.enabled': [getIIIFEnabled(true)],
|
||||||
|
'iiif.search.enabled': [getIIIFSearchEnabled(true)],
|
||||||
|
};
|
||||||
|
TestBed.overrideProvider(RouteService, {useValue: localMockRouteService});
|
||||||
|
TestBed.compileComponents();
|
||||||
fixture = TestBed.createComponent(UntypedItemComponent);
|
fixture = TestBed.createComponent(UntypedItemComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
comp.object = getItem(iiifEnabledWithSearchMap);
|
comp.object = getItem(iiifEnabledMap);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -187,9 +224,12 @@ describe('UntypedItemComponent', () => {
|
|||||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call the RouteService getHistory method', () => {
|
it('should not retrieve the query term for previous route', fakeAsync( () => {
|
||||||
expect(mockRouteService.getPreviousUrl).toHaveBeenCalled();
|
let emitted;
|
||||||
});
|
comp.iiifQuery$.subscribe(result => emitted = result);
|
||||||
|
tick(10);
|
||||||
|
expect(emitted).toBeUndefined();
|
||||||
|
}));
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -62,6 +62,8 @@ export class VersionedItemComponent extends ItemComponent {
|
|||||||
activeModal.componentInstance.createVersionEvent.pipe(
|
activeModal.componentInstance.createVersionEvent.pipe(
|
||||||
switchMap((summary: string) => this.versionHistoryService.createVersion(item._links.self.href, summary)),
|
switchMap((summary: string) => this.versionHistoryService.createVersion(item._links.self.href, summary)),
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
|
// close model (should be displaying loading/waiting indicator) when version creation failed/succeeded
|
||||||
|
tap(() => activeModal.close()),
|
||||||
// show success/failure notification
|
// show success/failure notification
|
||||||
tap((res: RemoteData<Version>) => { this.itemVersionShared.notifyCreateNewVersion(res); }),
|
tap((res: RemoteData<Version>) => { this.itemVersionShared.notifyCreateNewVersion(res); }),
|
||||||
// get workspace item
|
// get workspace item
|
||||||
|
10
src/app/page-error/page-error.component.html
Normal file
10
src/app/page-error/page-error.component.html
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<div class="page-internal-server-error container">
|
||||||
|
<h1 data-test="status">{{status}}</h1>
|
||||||
|
<h2><small>{{"error-page.description." + status | translate}}</small></h2>
|
||||||
|
<br/>
|
||||||
|
<p>{{"error-page." + code | translate}}</p>
|
||||||
|
<br/>
|
||||||
|
<p class="text-center">
|
||||||
|
<a href="/home" class="btn btn-primary">{{ status + ".link.home-page" | translate}}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
0
src/app/page-error/page-error.component.scss
Normal file
0
src/app/page-error/page-error.component.scss
Normal file
48
src/app/page-error/page-error.component.spec.ts
Normal file
48
src/app/page-error/page-error.component.spec.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { PageErrorComponent } from './page-error.component';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { ActivatedRouteStub } from '../shared/testing/active-router.stub';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { TranslateLoaderMock } from '../shared/testing/translate-loader.mock';
|
||||||
|
|
||||||
|
describe('PageErrorComponent', () => {
|
||||||
|
let component: PageErrorComponent;
|
||||||
|
let fixture: ComponentFixture<PageErrorComponent>;
|
||||||
|
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
||||||
|
queryParams: observableOf({
|
||||||
|
status: 401,
|
||||||
|
code: 'orcid.generic-error'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ PageErrorComponent ],
|
||||||
|
imports: [
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(PageErrorComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show error for 401 unauthorized', () => {
|
||||||
|
const statusElement = fixture.debugElement.query(By.css('[data-test="status"]')).nativeElement;
|
||||||
|
expect(statusElement.innerHTML).toEqual('401');
|
||||||
|
});
|
||||||
|
});
|
27
src/app/page-error/page-error.component.ts
Normal file
27
src/app/page-error/page-error.component.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component representing the `PageError` DSpace page.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-page-error',
|
||||||
|
styleUrls: ['./page-error.component.scss'],
|
||||||
|
templateUrl: './page-error.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.Default
|
||||||
|
})
|
||||||
|
export class PageErrorComponent {
|
||||||
|
status: number;
|
||||||
|
code: string;
|
||||||
|
/**
|
||||||
|
* Initialize instance variables
|
||||||
|
*
|
||||||
|
* @param {ActivatedRoute} activatedRoute
|
||||||
|
*/
|
||||||
|
constructor(private activatedRoute: ActivatedRoute) {
|
||||||
|
this.activatedRoute.queryParams.subscribe((params) => {
|
||||||
|
this.status = params.status;
|
||||||
|
this.code = params.code;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
26
src/app/page-error/themed-page-error.component.ts
Normal file
26
src/app/page-error/themed-page-error.component.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ThemedComponent } from '../shared/theme-support/themed.component';
|
||||||
|
import { PageErrorComponent } from './page-error.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Themed wrapper for PageErrorComponent
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-search-page',
|
||||||
|
styleUrls: [],
|
||||||
|
templateUrl: '../shared/theme-support/themed.component.html',
|
||||||
|
})
|
||||||
|
export class ThemedPageErrorComponent extends ThemedComponent<PageErrorComponent> {
|
||||||
|
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'PageErrorComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../themes/${themeName}/app/page-error/page-error.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import(`src/app/page-error/page-error.component`);
|
||||||
|
}
|
||||||
|
}
|
@@ -40,6 +40,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
PageInternalServerErrorComponent
|
PageInternalServerErrorComponent
|
||||||
} from './page-internal-server-error/page-internal-server-error.component';
|
} from './page-internal-server-error/page-internal-server-error.component';
|
||||||
|
import { ThemedPageErrorComponent } from './page-error/themed-page-error.component';
|
||||||
|
import { PageErrorComponent } from './page-error/page-error.component';
|
||||||
|
|
||||||
const IMPORTS = [
|
const IMPORTS = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -74,7 +76,9 @@ const DECLARATIONS = [
|
|||||||
ThemedForbiddenComponent,
|
ThemedForbiddenComponent,
|
||||||
IdleModalComponent,
|
IdleModalComponent,
|
||||||
ThemedPageInternalServerErrorComponent,
|
ThemedPageInternalServerErrorComponent,
|
||||||
PageInternalServerErrorComponent
|
PageInternalServerErrorComponent,
|
||||||
|
ThemedPageErrorComponent,
|
||||||
|
PageErrorComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
const EXPORTS = [
|
const EXPORTS = [
|
||||||
|
@@ -1,3 +1 @@
|
|||||||
.btn-dark {
|
|
||||||
background-color: var(--ds-admin-sidebar-bg);
|
|
||||||
}
|
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
<a *ngIf="isAuthorized | async"
|
||||||
|
[ngbTooltip]="'item.page.orcid.tooltip' | translate"
|
||||||
|
[routerLink]="[pageRoute, 'orcid']"
|
||||||
|
class="btn btn-dark btn-sm"
|
||||||
|
role="button" ><i class="fab fa-orcid fa-lg"></i>
|
||||||
|
</a>
|
@@ -0,0 +1,76 @@
|
|||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { DsoPageOrcidButtonComponent } from './dso-page-orcid-button.component';
|
||||||
|
|
||||||
|
describe('DsoPageOrcidButtonComponent', () => {
|
||||||
|
let component: DsoPageOrcidButtonComponent;
|
||||||
|
let fixture: ComponentFixture<DsoPageOrcidButtonComponent>;
|
||||||
|
|
||||||
|
let authorizationService: AuthorizationDataService;
|
||||||
|
let dso: DSpaceObject;
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
dso = Object.assign(new Item(), {
|
||||||
|
id: 'test-item',
|
||||||
|
_links: {
|
||||||
|
self: { href: 'test-item-selflink' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
|
isAuthorized: observableOf(true)
|
||||||
|
});
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [DsoPageOrcidButtonComponent],
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NgbModule],
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationService }
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(DsoPageOrcidButtonComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.dso = dso;
|
||||||
|
component.pageRoute = 'test';
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check the authorization of the current user', () => {
|
||||||
|
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(FeatureID.CanSynchronizeWithORCID, dso.self);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the user is authorized', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(true));
|
||||||
|
component.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a link', () => {
|
||||||
|
const link = fixture.debugElement.query(By.css('a'));
|
||||||
|
expect(link).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the user is not authorized', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
|
||||||
|
component.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render a link', () => {
|
||||||
|
const link = fixture.debugElement.query(By.css('a'));
|
||||||
|
expect(link).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,39 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||||
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-dso-page-orcid-button',
|
||||||
|
templateUrl: './dso-page-orcid-button.component.html',
|
||||||
|
styleUrls: ['./dso-page-orcid-button.component.scss']
|
||||||
|
})
|
||||||
|
export class DsoPageOrcidButtonComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* The DSpaceObject to display a button to the edit page for
|
||||||
|
*/
|
||||||
|
@Input() dso: DSpaceObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The prefix of the route to the edit page (before the object's UUID, e.g. "items")
|
||||||
|
*/
|
||||||
|
@Input() pageRoute: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the current user is authorized to edit the DSpaceObject
|
||||||
|
*/
|
||||||
|
isAuthorized: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||||
|
|
||||||
|
constructor(protected authorizationService: AuthorizationDataService) { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.authorizationService.isAuthorized(FeatureID.CanSynchronizeWithORCID, this.dso.self).pipe(take(1)).subscribe((isAuthorized: boolean) => {
|
||||||
|
this.isAuthorized.next(isAuthorized);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1 +1,7 @@
|
|||||||
<button class="edit-button btn btn-dark btn-sm" data-test="item-claim" *ngIf="(isClaimable() | async)" (click)="claim()"> {{"item.page.claim.button" | translate }} </button>
|
<button *ngIf="(isClaimable() | async)"
|
||||||
|
[ngbTooltip]="'item.page.claim.tooltip' | translate"
|
||||||
|
class="edit-button btn btn-dark btn-sm"
|
||||||
|
data-test="item-claim"
|
||||||
|
(click)="claim()">
|
||||||
|
{{'item.page.claim.button' | translate }}
|
||||||
|
</button>
|
||||||
|
@@ -27,7 +27,7 @@
|
|||||||
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">
|
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">
|
||||||
<div class="dropdown-list">
|
<div class="dropdown-list">
|
||||||
<div *ngFor="let suggestionOption of suggestions">
|
<div *ngFor="let suggestionOption of suggestions">
|
||||||
<a href="javascript:void(0);" class="d-block dropdown-item" (click)="onClickSuggestion(suggestionOption.value)" #suggestion>
|
<a href="javascript:void(0);" class="d-block dropdown-item" (click)="onClickSuggestion(suggestionOption)" #suggestion>
|
||||||
<span [innerHTML]="suggestionOption.displayValue"></span>
|
<span [innerHTML]="suggestionOption.displayValue"></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -51,7 +51,7 @@ describe('FilterInputSuggestionsComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('should call onClickSuggestion() with the suggestion as a parameter', () => {
|
it('should call onClickSuggestion() with the suggestion as a parameter', () => {
|
||||||
expect(comp.onClickSuggestion).toHaveBeenCalledWith(suggestions[clickedIndex].value);
|
expect(comp.onClickSuggestion).toHaveBeenCalledWith(suggestions[clickedIndex]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -32,8 +32,8 @@ export class FilterInputSuggestionsComponent extends InputSuggestionsComponent {
|
|||||||
this.submitSuggestion.emit(data);
|
this.submitSuggestion.emit(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickSuggestion(data) {
|
onClickSuggestion(data: InputSuggestion) {
|
||||||
this.value = data;
|
this.value = data.value;
|
||||||
this.clickSuggestion.emit(data);
|
this.clickSuggestion.emit(data);
|
||||||
this.close();
|
this.close();
|
||||||
this.blockReopen = true;
|
this.blockReopen = true;
|
||||||
|
@@ -7,6 +7,14 @@ export interface InputSuggestion {
|
|||||||
*/
|
*/
|
||||||
displayValue: string;
|
displayValue: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The search query that can be used with filter suggestion.
|
||||||
|
* It contains the value within the operator :
|
||||||
|
* - value,equals
|
||||||
|
* - value,authority
|
||||||
|
*/
|
||||||
|
query?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The actual value of the suggestion
|
* The actual value of the suggestion
|
||||||
*/
|
*/
|
||||||
|
25
src/app/shared/interfaces/modal-before-dismiss.interface.ts
Normal file
25
src/app/shared/interfaces/modal-before-dismiss.interface.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { NgbModalConfig, NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a component implementing this interface is used to create a modal (i.e. it is passed to {@link NgbModal#open}),
|
||||||
|
* and that modal is dismissed, then {@link #beforeDismiss} will be called.
|
||||||
|
*
|
||||||
|
* This behavior is implemented for the whole app, by setting a default value for {@link NgbModalConfig#beforeDismiss}
|
||||||
|
* in {@link AppComponent}.
|
||||||
|
*
|
||||||
|
* Docs: https://ng-bootstrap.github.io/#/components/modal/api
|
||||||
|
*/
|
||||||
|
export interface ModalBeforeDismiss {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback right before the modal will be dismissed.
|
||||||
|
* If this function returns:
|
||||||
|
* - false
|
||||||
|
* - a promise resolved with false
|
||||||
|
* - a promise that is rejected
|
||||||
|
* then the modal won't be dismissed.
|
||||||
|
* Docs: https://ng-bootstrap.github.io/#/components/modal/api#NgbModalOptions
|
||||||
|
*/
|
||||||
|
beforeDismiss(): boolean | Promise<boolean>;
|
||||||
|
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
<div>
|
<div *ngIf="!(this.submitted$ | async); else waiting">
|
||||||
<div class="modal-header">{{'item.version.create.modal.header' | translate}}
|
<div class="modal-header">{{'item.version.create.modal.header' | translate}}
|
||||||
<button type="button" class="close" (click)="onModalClose()" aria-label="Close">
|
<button type="button" class="close" (click)="onModalClose()" aria-label="Close">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
@@ -34,3 +34,13 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ng-template #waiting>
|
||||||
|
<div class="modal-header">{{'item.version.create.modal.submitted.header' | translate}}</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>{{'item.version.create.modal.submitted.text' | translate}}</p>
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<ds-loading [showMessage]="false"></ds-loading>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
@@ -1,16 +1,19 @@
|
|||||||
import { Component, EventEmitter, Output } from '@angular/core';
|
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
import { ModalBeforeDismiss } from '../../../interfaces/modal-before-dismiss.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-versions-summary-modal',
|
selector: 'ds-item-versions-summary-modal',
|
||||||
templateUrl: './item-versions-summary-modal.component.html',
|
templateUrl: './item-versions-summary-modal.component.html',
|
||||||
styleUrls: ['./item-versions-summary-modal.component.scss']
|
styleUrls: ['./item-versions-summary-modal.component.scss']
|
||||||
})
|
})
|
||||||
export class ItemVersionsSummaryModalComponent {
|
export class ItemVersionsSummaryModalComponent implements OnInit, ModalBeforeDismiss {
|
||||||
|
|
||||||
versionNumber: number;
|
versionNumber: number;
|
||||||
newVersionSummary: string;
|
newVersionSummary: string;
|
||||||
firstVersion = true;
|
firstVersion = true;
|
||||||
|
submitted$: BehaviorSubject<boolean>;
|
||||||
|
|
||||||
@Output() createVersionEvent: EventEmitter<string> = new EventEmitter<string>();
|
@Output() createVersionEvent: EventEmitter<string> = new EventEmitter<string>();
|
||||||
|
|
||||||
@@ -19,13 +22,24 @@ export class ItemVersionsSummaryModalComponent {
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.submitted$ = new BehaviorSubject<boolean>(false);
|
||||||
|
}
|
||||||
|
|
||||||
onModalClose() {
|
onModalClose() {
|
||||||
this.activeModal.dismiss();
|
this.activeModal.dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
beforeDismiss(): boolean | Promise<boolean> {
|
||||||
|
// prevent the modal from being dismissed after version creation is initiated
|
||||||
|
return !this.submitted$.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
onModalSubmit() {
|
onModalSubmit() {
|
||||||
this.createVersionEvent.emit(this.newVersionSummary);
|
this.createVersionEvent.emit(this.newVersionSummary);
|
||||||
this.activeModal.close();
|
this.submitted$.next(true);
|
||||||
|
// NOTE: the caller of this modal is responsible for closing it,
|
||||||
|
// e.g. after the version creation POST request completed.
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -340,6 +340,9 @@ export class ItemVersionsComponent implements OnInit {
|
|||||||
version.item.pipe(getFirstSucceededRemoteDataPayload())
|
version.item.pipe(getFirstSucceededRemoteDataPayload())
|
||||||
])),
|
])),
|
||||||
mergeMap(([summary, item]: [string, Item]) => this.versionHistoryService.createVersion(item._links.self.href, summary)),
|
mergeMap(([summary, item]: [string, Item]) => this.versionHistoryService.createVersion(item._links.self.href, summary)),
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
// close model (should be displaying loading/waiting indicator) when version creation failed/succeeded
|
||||||
|
tap(() => activeModal.close()),
|
||||||
// show success/failure notification
|
// show success/failure notification
|
||||||
tap((newVersionRD: RemoteData<Version>) => {
|
tap((newVersionRD: RemoteData<Version>) => {
|
||||||
this.itemVersionShared.notifyCreateNewVersion(newVersionRD);
|
this.itemVersionShared.notifyCreateNewVersion(newVersionRD);
|
||||||
|
@@ -0,0 +1,110 @@
|
|||||||
|
import { Component, Inject, OnInit, } from '@angular/core';
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { select, Store } from '@ngrx/store';
|
||||||
|
|
||||||
|
import { AuthMethod } from '../../../core/auth/models/auth.method';
|
||||||
|
|
||||||
|
import { isAuthenticated, isAuthenticationLoading } from '../../../core/auth/selectors';
|
||||||
|
import { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service';
|
||||||
|
import { isEmpty, isNotNull } from '../../empty.util';
|
||||||
|
import { AuthService } from '../../../core/auth/auth.service';
|
||||||
|
import { HardRedirectService } from '../../../core/services/hard-redirect.service';
|
||||||
|
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
|
||||||
|
import { CoreState } from '../../../core/core-state.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-log-in-external-provider',
|
||||||
|
template: ''
|
||||||
|
|
||||||
|
})
|
||||||
|
export abstract class LogInExternalProviderComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The authentication method data.
|
||||||
|
* @type {AuthMethod}
|
||||||
|
*/
|
||||||
|
public authMethod: AuthMethod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the authentication is loading.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
public loading: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The shibboleth authentication location url.
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
public location: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether user is authenticated.
|
||||||
|
* @type {Observable<string>}
|
||||||
|
*/
|
||||||
|
public isAuthenticated: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
* @param {AuthMethod} injectedAuthMethodModel
|
||||||
|
* @param {boolean} isStandalonePage
|
||||||
|
* @param {NativeWindowRef} _window
|
||||||
|
* @param {AuthService} authService
|
||||||
|
* @param {HardRedirectService} hardRedirectService
|
||||||
|
* @param {Store<State>} store
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
@Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod,
|
||||||
|
@Inject('isStandalonePage') public isStandalonePage: boolean,
|
||||||
|
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
||||||
|
private authService: AuthService,
|
||||||
|
private hardRedirectService: HardRedirectService,
|
||||||
|
private store: Store<CoreState>
|
||||||
|
) {
|
||||||
|
this.authMethod = injectedAuthMethodModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
// set isAuthenticated
|
||||||
|
this.isAuthenticated = this.store.pipe(select(isAuthenticated));
|
||||||
|
|
||||||
|
// set loading
|
||||||
|
this.loading = this.store.pipe(select(isAuthenticationLoading));
|
||||||
|
|
||||||
|
// set location
|
||||||
|
this.location = decodeURIComponent(this.injectedAuthMethodModel.location);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect to the external provider url for login
|
||||||
|
*/
|
||||||
|
redirectToExternalProvider() {
|
||||||
|
this.authService.getRedirectUrl().pipe(take(1)).subscribe((redirectRoute) => {
|
||||||
|
if (!this.isStandalonePage) {
|
||||||
|
redirectRoute = this.hardRedirectService.getCurrentRoute();
|
||||||
|
} else if (isEmpty(redirectRoute)) {
|
||||||
|
redirectRoute = '/';
|
||||||
|
}
|
||||||
|
const correctRedirectUrl = new URLCombiner(this._window.nativeWindow.origin, redirectRoute).toString();
|
||||||
|
|
||||||
|
let externalServerUrl = this.location;
|
||||||
|
const myRegexp = /\?redirectUrl=(.*)/g;
|
||||||
|
const match = myRegexp.exec(this.location);
|
||||||
|
const redirectUrlFromServer = (match && match[1]) ? match[1] : null;
|
||||||
|
|
||||||
|
// Check whether the current page is different from the redirect url received from rest
|
||||||
|
if (isNotNull(redirectUrlFromServer) && redirectUrlFromServer !== correctRedirectUrl) {
|
||||||
|
// change the redirect url with the current page url
|
||||||
|
const newRedirectUrl = `?redirectUrl=${correctRedirectUrl}`;
|
||||||
|
externalServerUrl = this.location.replace(/\?redirectUrl=(.*)/g, newRedirectUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// redirect to shibboleth authentication url
|
||||||
|
this.hardRedirectService.redirect(externalServerUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,110 +1,21 @@
|
|||||||
import { Component, Inject, OnInit, } from '@angular/core';
|
import { Component, } from '@angular/core';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { select, Store } from '@ngrx/store';
|
|
||||||
|
|
||||||
import { renderAuthMethodFor } from '../log-in.methods-decorator';
|
import { renderAuthMethodFor } from '../log-in.methods-decorator';
|
||||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||||
import { AuthMethod } from '../../../../core/auth/models/auth.method';
|
import { LogInExternalProviderComponent } from '../log-in-external-provider.component';
|
||||||
|
|
||||||
import { isAuthenticated, isAuthenticationLoading } from '../../../../core/auth/selectors';
|
|
||||||
import { NativeWindowRef, NativeWindowService } from '../../../../core/services/window.service';
|
|
||||||
import { isNotNull, isEmpty } from '../../../empty.util';
|
|
||||||
import { AuthService } from '../../../../core/auth/auth.service';
|
|
||||||
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
|
|
||||||
import { take } from 'rxjs/operators';
|
|
||||||
import { URLCombiner } from '../../../../core/url-combiner/url-combiner';
|
|
||||||
import { CoreState } from '../../../../core/core-state.model';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-log-in-oidc',
|
selector: 'ds-log-in-oidc',
|
||||||
templateUrl: './log-in-oidc.component.html',
|
templateUrl: './log-in-oidc.component.html',
|
||||||
})
|
})
|
||||||
@renderAuthMethodFor(AuthMethodType.Oidc)
|
@renderAuthMethodFor(AuthMethodType.Oidc)
|
||||||
export class LogInOidcComponent implements OnInit {
|
export class LogInOidcComponent extends LogInExternalProviderComponent {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The authentication method data.
|
* Redirect to orcid authentication url
|
||||||
* @type {AuthMethod}
|
|
||||||
*/
|
*/
|
||||||
public authMethod: AuthMethod;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* True if the authentication is loading.
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
public loading: Observable<boolean>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The oidc authentication location url.
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
public location: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether user is authenticated.
|
|
||||||
* @type {Observable<string>}
|
|
||||||
*/
|
|
||||||
public isAuthenticated: Observable<boolean>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @constructor
|
|
||||||
* @param {AuthMethod} injectedAuthMethodModel
|
|
||||||
* @param {boolean} isStandalonePage
|
|
||||||
* @param {NativeWindowRef} _window
|
|
||||||
* @param {AuthService} authService
|
|
||||||
* @param {HardRedirectService} hardRedirectService
|
|
||||||
* @param {Store<State>} store
|
|
||||||
*/
|
|
||||||
constructor(
|
|
||||||
@Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod,
|
|
||||||
@Inject('isStandalonePage') public isStandalonePage: boolean,
|
|
||||||
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
|
||||||
private authService: AuthService,
|
|
||||||
private hardRedirectService: HardRedirectService,
|
|
||||||
private store: Store<CoreState>
|
|
||||||
) {
|
|
||||||
this.authMethod = injectedAuthMethodModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
// set isAuthenticated
|
|
||||||
this.isAuthenticated = this.store.pipe(select(isAuthenticated));
|
|
||||||
|
|
||||||
// set loading
|
|
||||||
this.loading = this.store.pipe(select(isAuthenticationLoading));
|
|
||||||
|
|
||||||
// set location
|
|
||||||
this.location = decodeURIComponent(this.injectedAuthMethodModel.location);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
redirectToOidc() {
|
redirectToOidc() {
|
||||||
|
this.redirectToExternalProvider();
|
||||||
this.authService.getRedirectUrl().pipe(take(1)).subscribe((redirectRoute) => {
|
|
||||||
if (!this.isStandalonePage) {
|
|
||||||
redirectRoute = this.hardRedirectService.getCurrentRoute();
|
|
||||||
} else if (isEmpty(redirectRoute)) {
|
|
||||||
redirectRoute = '/';
|
|
||||||
}
|
|
||||||
const correctRedirectUrl = new URLCombiner(this._window.nativeWindow.origin, redirectRoute).toString();
|
|
||||||
|
|
||||||
let oidcServerUrl = this.location;
|
|
||||||
const myRegexp = /\?redirectUrl=(.*)/g;
|
|
||||||
const match = myRegexp.exec(this.location);
|
|
||||||
const redirectUrlFromServer = (match && match[1]) ? match[1] : null;
|
|
||||||
|
|
||||||
// Check whether the current page is different from the redirect url received from rest
|
|
||||||
if (isNotNull(redirectUrlFromServer) && redirectUrlFromServer !== correctRedirectUrl) {
|
|
||||||
// change the redirect url with the current page url
|
|
||||||
const newRedirectUrl = `?redirectUrl=${correctRedirectUrl}`;
|
|
||||||
oidcServerUrl = this.location.replace(/\?redirectUrl=(.*)/g, newRedirectUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
// redirect to oidc authentication url
|
|
||||||
this.hardRedirectService.redirect(oidcServerUrl);
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
<button class="btn btn-lg btn-primary btn-block mt-2 text-white" (click)="redirectToOrcid()">
|
||||||
|
<i class="fas fa-sign-in-alt"></i> {{"login.form.orcid" | translate}}
|
||||||
|
</button>
|
@@ -0,0 +1,155 @@
|
|||||||
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
|
import { Store, StoreModule } from '@ngrx/store';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
||||||
|
import { EPersonMock } from '../../../testing/eperson.mock';
|
||||||
|
import { authReducer } from '../../../../core/auth/auth.reducer';
|
||||||
|
import { AuthService } from '../../../../core/auth/auth.service';
|
||||||
|
import { AuthServiceStub } from '../../../testing/auth-service.stub';
|
||||||
|
import { storeModuleConfig } from '../../../../app.reducer';
|
||||||
|
import { AuthMethod } from '../../../../core/auth/models/auth.method';
|
||||||
|
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||||
|
import { LogInOrcidComponent } from './log-in-orcid.component';
|
||||||
|
import { NativeWindowService } from '../../../../core/services/window.service';
|
||||||
|
import { RouterStub } from '../../../testing/router.stub';
|
||||||
|
import { ActivatedRouteStub } from '../../../testing/active-router.stub';
|
||||||
|
import { NativeWindowMockFactory } from '../../../mocks/mock-native-window-ref';
|
||||||
|
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
|
||||||
|
|
||||||
|
|
||||||
|
describe('LogInOrcidComponent', () => {
|
||||||
|
|
||||||
|
let component: LogInOrcidComponent;
|
||||||
|
let fixture: ComponentFixture<LogInOrcidComponent>;
|
||||||
|
let page: Page;
|
||||||
|
let user: EPerson;
|
||||||
|
let componentAsAny: any;
|
||||||
|
let setHrefSpy;
|
||||||
|
let orcidBaseUrl;
|
||||||
|
let location;
|
||||||
|
let initialState: any;
|
||||||
|
let hardRedirectService: HardRedirectService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
user = EPersonMock;
|
||||||
|
orcidBaseUrl = 'dspace-rest.test/orcid?redirectUrl=';
|
||||||
|
location = orcidBaseUrl + 'http://dspace-angular.test/home';
|
||||||
|
|
||||||
|
hardRedirectService = jasmine.createSpyObj('hardRedirectService', {
|
||||||
|
getCurrentRoute: {},
|
||||||
|
redirect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
initialState = {
|
||||||
|
core: {
|
||||||
|
auth: {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
blocking: false,
|
||||||
|
loading: false,
|
||||||
|
authMethods: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
// refine the test module by declaring the test component
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
StoreModule.forRoot({ auth: authReducer }, storeModuleConfig),
|
||||||
|
TranslateModule.forRoot()
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
LogInOrcidComponent
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthService, useClass: AuthServiceStub },
|
||||||
|
{ provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Orcid, location) },
|
||||||
|
{ provide: 'isStandalonePage', useValue: true },
|
||||||
|
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
|
||||||
|
{ provide: Router, useValue: new RouterStub() },
|
||||||
|
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
||||||
|
{ provide: HardRedirectService, useValue: hardRedirectService },
|
||||||
|
provideMockStore({ initialState }),
|
||||||
|
],
|
||||||
|
schemas: [
|
||||||
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// create component and test fixture
|
||||||
|
fixture = TestBed.createComponent(LogInOrcidComponent);
|
||||||
|
|
||||||
|
// get test component from the fixture
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
componentAsAny = component;
|
||||||
|
|
||||||
|
// create page
|
||||||
|
page = new Page(component, fixture);
|
||||||
|
setHrefSpy = spyOnProperty(componentAsAny._window.nativeWindow.location, 'href', 'set').and.callThrough();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the properly a new redirectUrl', () => {
|
||||||
|
const currentUrl = 'http://dspace-angular.test/collections/12345';
|
||||||
|
componentAsAny._window.nativeWindow.location.href = currentUrl;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(componentAsAny.injectedAuthMethodModel.location).toBe(location);
|
||||||
|
expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl);
|
||||||
|
|
||||||
|
component.redirectToOrcid();
|
||||||
|
|
||||||
|
expect(setHrefSpy).toHaveBeenCalledWith(currentUrl);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set a new redirectUrl', () => {
|
||||||
|
const currentUrl = 'http://dspace-angular.test/home';
|
||||||
|
componentAsAny._window.nativeWindow.location.href = currentUrl;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(componentAsAny.injectedAuthMethodModel.location).toBe(location);
|
||||||
|
expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl);
|
||||||
|
|
||||||
|
component.redirectToOrcid();
|
||||||
|
|
||||||
|
expect(setHrefSpy).toHaveBeenCalledWith(currentUrl);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* I represent the DOM elements and attach spies.
|
||||||
|
*
|
||||||
|
* @class Page
|
||||||
|
*/
|
||||||
|
class Page {
|
||||||
|
|
||||||
|
public emailInput: HTMLInputElement;
|
||||||
|
public navigateSpy: jasmine.Spy;
|
||||||
|
public passwordInput: HTMLInputElement;
|
||||||
|
|
||||||
|
constructor(private component: LogInOrcidComponent, private fixture: ComponentFixture<LogInOrcidComponent>) {
|
||||||
|
// use injector to get services
|
||||||
|
const injector = fixture.debugElement.injector;
|
||||||
|
const store = injector.get(Store);
|
||||||
|
|
||||||
|
// add spies
|
||||||
|
this.navigateSpy = spyOn(store, 'dispatch');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,21 @@
|
|||||||
|
import { Component, } from '@angular/core';
|
||||||
|
|
||||||
|
import { renderAuthMethodFor } from '../log-in.methods-decorator';
|
||||||
|
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||||
|
import { LogInExternalProviderComponent } from '../log-in-external-provider.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-log-in-orcid',
|
||||||
|
templateUrl: './log-in-orcid.component.html',
|
||||||
|
})
|
||||||
|
@renderAuthMethodFor(AuthMethodType.Orcid)
|
||||||
|
export class LogInOrcidComponent extends LogInExternalProviderComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect to orcid authentication url
|
||||||
|
*/
|
||||||
|
redirectToOrcid() {
|
||||||
|
this.redirectToExternalProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,21 +1,8 @@
|
|||||||
import { Component, Inject, OnInit, } from '@angular/core';
|
import { Component, } from '@angular/core';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { select, Store } from '@ngrx/store';
|
|
||||||
|
|
||||||
import { renderAuthMethodFor } from '../log-in.methods-decorator';
|
import { renderAuthMethodFor } from '../log-in.methods-decorator';
|
||||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||||
import { AuthMethod } from '../../../../core/auth/models/auth.method';
|
import { LogInExternalProviderComponent } from '../log-in-external-provider.component';
|
||||||
|
|
||||||
import { isAuthenticated, isAuthenticationLoading } from '../../../../core/auth/selectors';
|
|
||||||
import { RouteService } from '../../../../core/services/route.service';
|
|
||||||
import { NativeWindowRef, NativeWindowService } from '../../../../core/services/window.service';
|
|
||||||
import { isNotNull, isEmpty } from '../../../empty.util';
|
|
||||||
import { AuthService } from '../../../../core/auth/auth.service';
|
|
||||||
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
|
|
||||||
import { take } from 'rxjs/operators';
|
|
||||||
import { URLCombiner } from '../../../../core/url-combiner/url-combiner';
|
|
||||||
import { CoreState } from '../../../../core/core-state.model';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-log-in-shibboleth',
|
selector: 'ds-log-in-shibboleth',
|
||||||
@@ -24,92 +11,13 @@ import { CoreState } from '../../../../core/core-state.model';
|
|||||||
|
|
||||||
})
|
})
|
||||||
@renderAuthMethodFor(AuthMethodType.Shibboleth)
|
@renderAuthMethodFor(AuthMethodType.Shibboleth)
|
||||||
export class LogInShibbolethComponent implements OnInit {
|
export class LogInShibbolethComponent extends LogInExternalProviderComponent {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The authentication method data.
|
* Redirect to shibboleth authentication url
|
||||||
* @type {AuthMethod}
|
|
||||||
*/
|
*/
|
||||||
public authMethod: AuthMethod;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* True if the authentication is loading.
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
public loading: Observable<boolean>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The shibboleth authentication location url.
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
public location: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether user is authenticated.
|
|
||||||
* @type {Observable<string>}
|
|
||||||
*/
|
|
||||||
public isAuthenticated: Observable<boolean>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @constructor
|
|
||||||
* @param {AuthMethod} injectedAuthMethodModel
|
|
||||||
* @param {boolean} isStandalonePage
|
|
||||||
* @param {NativeWindowRef} _window
|
|
||||||
* @param {RouteService} route
|
|
||||||
* @param {AuthService} authService
|
|
||||||
* @param {HardRedirectService} hardRedirectService
|
|
||||||
* @param {Store<State>} store
|
|
||||||
*/
|
|
||||||
constructor(
|
|
||||||
@Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod,
|
|
||||||
@Inject('isStandalonePage') public isStandalonePage: boolean,
|
|
||||||
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
|
||||||
private route: RouteService,
|
|
||||||
private authService: AuthService,
|
|
||||||
private hardRedirectService: HardRedirectService,
|
|
||||||
private store: Store<CoreState>
|
|
||||||
) {
|
|
||||||
this.authMethod = injectedAuthMethodModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
// set isAuthenticated
|
|
||||||
this.isAuthenticated = this.store.pipe(select(isAuthenticated));
|
|
||||||
|
|
||||||
// set loading
|
|
||||||
this.loading = this.store.pipe(select(isAuthenticationLoading));
|
|
||||||
|
|
||||||
// set location
|
|
||||||
this.location = decodeURIComponent(this.injectedAuthMethodModel.location);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
redirectToShibboleth() {
|
redirectToShibboleth() {
|
||||||
|
this.redirectToExternalProvider();
|
||||||
this.authService.getRedirectUrl().pipe(take(1)).subscribe((redirectRoute) => {
|
|
||||||
if (!this.isStandalonePage) {
|
|
||||||
redirectRoute = this.hardRedirectService.getCurrentRoute();
|
|
||||||
} else if (isEmpty(redirectRoute)) {
|
|
||||||
redirectRoute = '/';
|
|
||||||
}
|
|
||||||
const correctRedirectUrl = new URLCombiner(this._window.nativeWindow.origin, redirectRoute).toString();
|
|
||||||
|
|
||||||
let shibbolethServerUrl = this.location;
|
|
||||||
const myRegexp = /\?redirectUrl=(.*)/g;
|
|
||||||
const match = myRegexp.exec(this.location);
|
|
||||||
const redirectUrlFromServer = (match && match[1]) ? match[1] : null;
|
|
||||||
|
|
||||||
// Check whether the current page is different from the redirect url received from rest
|
|
||||||
if (isNotNull(redirectUrlFromServer) && redirectUrlFromServer !== correctRedirectUrl) {
|
|
||||||
// change the redirect url with the current page url
|
|
||||||
const newRedirectUrl = `?redirectUrl=${correctRedirectUrl}`;
|
|
||||||
shibbolethServerUrl = this.location.replace(/\?redirectUrl=(.*)/g, newRedirectUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
// redirect to shibboleth authentication url
|
|
||||||
this.hardRedirectService.redirect(shibbolethServerUrl);
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -29,4 +29,8 @@ export class RouterMock {
|
|||||||
createUrlTree(commands, navExtras = {}) {
|
createUrlTree(commands, navExtras = {}) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get url() {
|
||||||
|
return this.routerState.snapshot.url;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user