Merge branch 'main' into coar-notify-7

This commit is contained in:
FrancescoMolinaro
2024-02-28 15:41:07 +01:00
56 changed files with 1173 additions and 360 deletions

View File

@@ -1,9 +1,7 @@
import { URLCombiner } from '../../core/url-combiner/url-combiner';
import { getNotificationsModuleRoute } from '../admin-routing-paths';
export const QUALITY_ASSURANCE_EDIT_PATH = 'quality-assurance'; export const QUALITY_ASSURANCE_EDIT_PATH = 'quality-assurance';
export const PUBLICATION_CLAIMS_PATH = 'publication-claim'; export const PUBLICATION_CLAIMS_PATH = 'publication-claim';
export function getQualityAssuranceRoute(id: string) { export function getQualityAssuranceEditRoute() {
return new URLCombiner(getNotificationsModuleRoute(), QUALITY_ASSURANCE_EDIT_PATH, id).toString(); return `/${QUALITY_ASSURANCE_EDIT_PATH}`;
} }

View File

@@ -6,34 +6,22 @@ import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.r
import { I18nBreadcrumbsService } from '../../core/breadcrumbs/i18n-breadcrumbs.service'; import { I18nBreadcrumbsService } from '../../core/breadcrumbs/i18n-breadcrumbs.service';
import { PUBLICATION_CLAIMS_PATH } from './admin-notifications-routing-paths'; import { PUBLICATION_CLAIMS_PATH } from './admin-notifications-routing-paths';
import { AdminNotificationsPublicationClaimPageComponent } from './admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component'; import { AdminNotificationsPublicationClaimPageComponent } from './admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component';
import { AdminNotificationsPublicationClaimPageResolver } from './admin-notifications-publication-claim-page/admin-notifications-publication-claim-page-resolver.service';
import { QUALITY_ASSURANCE_EDIT_PATH } from './admin-notifications-routing-paths'; import { QUALITY_ASSURANCE_EDIT_PATH } from './admin-notifications-routing-paths';
import { AdminQualityAssuranceTopicsPageComponent } from './admin-quality-assurance-topics-page/admin-quality-assurance-topics-page.component';
import { AdminQualityAssuranceEventsPageComponent } from './admin-quality-assurance-events-page/admin-quality-assurance-events-page.component';
import { AdminQualityAssuranceTopicsPageResolver } from './admin-quality-assurance-topics-page/admin-quality-assurance-topics-page-resolver.service';
import { AdminQualityAssuranceEventsPageResolver } from './admin-quality-assurance-events-page/admin-quality-assurance-events-page.resolver';
import { AdminQualityAssuranceSourcePageComponent } from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-page.component';
import { AdminQualityAssuranceSourcePageResolver } from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-page-resolver.service';
import {
SiteAdministratorGuard
} from '../../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
import { QualityAssuranceBreadcrumbResolver } from '../../core/breadcrumbs/quality-assurance-breadcrumb.resolver'; import { QualityAssuranceBreadcrumbResolver } from '../../core/breadcrumbs/quality-assurance-breadcrumb.resolver';
import { QualityAssuranceBreadcrumbService } from '../../core/breadcrumbs/quality-assurance-breadcrumb.service'; import { QualityAssuranceBreadcrumbService } from '../../core/breadcrumbs/quality-assurance-breadcrumb.service';
import {
AdminNotificationsPublicationClaimPageResolver
} from '../../quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page-resolver.service';
import {
QualityAssuranceTopicsPageComponent
} from '../../quality-assurance-notifications-pages/quality-assurance-topics-page/quality-assurance-topics-page.component';
import {
QualityAssuranceTopicsPageResolver
} from '../../quality-assurance-notifications-pages/quality-assurance-topics-page/quality-assurance-topics-page-resolver.service';
import { import {
SourceDataResolver SourceDataResolver
} from '../../quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-data.resolver'; } from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-data.resolver';
import {
QualityAssuranceEventsPageResolver
} from '../../quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.resolver';
import {
QualityAssuranceEventsPageComponent
} from '../../quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.component';
import {
QualityAssuranceSourcePageResolver
} from '../../quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-page-resolver.service';
import {
QualityAssuranceSourcePageComponent
} from '../../quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-page.component';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -56,11 +44,11 @@ import {
{ {
canActivate: [ AuthenticatedGuard ], canActivate: [ AuthenticatedGuard ],
path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId`, path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId`,
component: QualityAssuranceTopicsPageComponent, component: AdminQualityAssuranceTopicsPageComponent,
pathMatch: 'full', pathMatch: 'full',
resolve: { resolve: {
breadcrumb: QualityAssuranceBreadcrumbResolver, breadcrumb: QualityAssuranceBreadcrumbResolver,
openaireQualityAssuranceTopicsParams: QualityAssuranceTopicsPageResolver openaireQualityAssuranceTopicsParams: AdminQualityAssuranceTopicsPageResolver
}, },
data: { data: {
title: 'admin.quality-assurance.page.title', title: 'admin.quality-assurance.page.title',
@@ -70,12 +58,27 @@ import {
}, },
{ {
canActivate: [ AuthenticatedGuard ], canActivate: [ AuthenticatedGuard ],
path: `${QUALITY_ASSURANCE_EDIT_PATH}`, path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId/target/:targetId`,
component: QualityAssuranceSourcePageComponent, component: AdminQualityAssuranceTopicsPageComponent,
pathMatch: 'full', pathMatch: 'full',
resolve: { resolve: {
breadcrumb: I18nBreadcrumbResolver, breadcrumb: I18nBreadcrumbResolver,
openaireQualityAssuranceSourceParams: QualityAssuranceSourcePageResolver, openaireQualityAssuranceTopicsParams: AdminQualityAssuranceTopicsPageResolver
},
data: {
title: 'admin.quality-assurance.page.title',
breadcrumbKey: 'admin.quality-assurance',
showBreadcrumbsFluid: false
}
},
{
canActivate: [ SiteAdministratorGuard ],
path: `${QUALITY_ASSURANCE_EDIT_PATH}`,
component: AdminQualityAssuranceSourcePageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: I18nBreadcrumbResolver,
openaireQualityAssuranceSourceParams: AdminQualityAssuranceSourcePageResolver,
sourceData: SourceDataResolver sourceData: SourceDataResolver
}, },
data: { data: {
@@ -87,11 +90,11 @@ import {
{ {
canActivate: [ AuthenticatedGuard ], canActivate: [ AuthenticatedGuard ],
path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId/:topicId`, path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId/:topicId`,
component: QualityAssuranceEventsPageComponent, component: AdminQualityAssuranceEventsPageComponent,
pathMatch: 'full', pathMatch: 'full',
resolve: { resolve: {
breadcrumb: QualityAssuranceBreadcrumbResolver, breadcrumb: QualityAssuranceBreadcrumbResolver,
openaireQualityAssuranceEventsParams: QualityAssuranceEventsPageResolver openaireQualityAssuranceEventsParams: AdminQualityAssuranceEventsPageResolver
}, },
data: { data: {
title: 'admin.notifications.event.page.title', title: 'admin.notifications.event.page.title',
@@ -106,10 +109,10 @@ import {
I18nBreadcrumbsService, I18nBreadcrumbsService,
AdminNotificationsPublicationClaimPageResolver, AdminNotificationsPublicationClaimPageResolver,
SourceDataResolver, SourceDataResolver,
QualityAssuranceSourcePageResolver, AdminQualityAssuranceSourcePageResolver,
QualityAssuranceTopicsPageResolver, AdminQualityAssuranceTopicsPageResolver,
QualityAssuranceEventsPageResolver, AdminQualityAssuranceEventsPageResolver,
QualityAssuranceSourcePageResolver, AdminQualityAssuranceSourcePageResolver,
QualityAssuranceBreadcrumbResolver, QualityAssuranceBreadcrumbResolver,
QualityAssuranceBreadcrumbService QualityAssuranceBreadcrumbService
] ]

View File

@@ -1,25 +1,18 @@
import { URLCombiner } from '../core/url-combiner/url-combiner'; import { URLCombiner } from '../core/url-combiner/url-combiner';
import { getAdminModuleRoute } from '../app-routing-paths'; import { getAdminModuleRoute } from '../app-routing-paths';
import { getQualityAssuranceEditRoute } from '../quality-assurance-notifications-pages/notifications-pages-routing-paths'; import { getQualityAssuranceEditRoute } from './admin-notifications/admin-notifications-routing-paths';
export const REGISTRIES_MODULE_PATH = 'registries'; export const REGISTRIES_MODULE_PATH = 'registries';
export const NOTIFICATIONS_MODULE_PATH = 'notifications'; export const NOTIFICATIONS_MODULE_PATH = 'notifications';
export const LDN_PATH = 'ldn';
export function getRegistriesModuleRoute() { export function getRegistriesModuleRoute() {
return new URLCombiner(getAdminModuleRoute(), REGISTRIES_MODULE_PATH).toString(); return new URLCombiner(getAdminModuleRoute(), REGISTRIES_MODULE_PATH).toString();
} }
export function getLdnServicesModuleRoute() { export function getNotificationsModuleRoute() {
return new URLCombiner(getAdminModuleRoute(), LDN_PATH).toString(); return new URLCombiner(getAdminModuleRoute(), NOTIFICATIONS_MODULE_PATH).toString();
} }
export function getNotificatioQualityAssuranceRoute() { export function getNotificatioQualityAssuranceRoute() {
return new URLCombiner(`/${NOTIFICATIONS_MODULE_PATH}`, getQualityAssuranceEditRoute()).toString(); return new URLCombiner(`/${NOTIFICATIONS_MODULE_PATH}`, getQualityAssuranceEditRoute()).toString();
} }
export function getNotificationsModuleRoute() {
return new URLCombiner(getAdminModuleRoute(), NOTIFICATIONS_MODULE_PATH).toString();
}

View File

@@ -6,72 +6,64 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component'; import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component'; import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
import { LDN_PATH, NOTIFICATIONS_MODULE_PATH, REGISTRIES_MODULE_PATH } from './admin-routing-paths'; import { REGISTRIES_MODULE_PATH } from './admin-routing-paths';
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component'; import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
import { NotifyInfoGuard } from '../core/coar-notify/notify-info/notify-info.guard'; import {
SiteAdministratorGuard
} from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
@NgModule({ @NgModule({
imports: [ imports: [
RouterModule.forChild([ RouterModule.forChild([
{
path: NOTIFICATIONS_MODULE_PATH,
loadChildren: () => import('./admin-notifications/admin-notifications.module')
.then((m) => m.AdminNotificationsModule),
},
{ {
path: REGISTRIES_MODULE_PATH, path: REGISTRIES_MODULE_PATH,
loadChildren: () => import('./admin-registries/admin-registries.module') loadChildren: () => import('./admin-registries/admin-registries.module')
.then((m) => m.AdminRegistriesModule), .then((m) => m.AdminRegistriesModule),
canActivate: [SiteAdministratorGuard]
}, },
{ {
path: 'search', path: 'search',
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: I18nBreadcrumbResolver },
component: AdminSearchPageComponent, component: AdminSearchPageComponent,
data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' } data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' },
canActivate: [SiteAdministratorGuard]
}, },
{ {
path: 'workflow', path: 'workflow',
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: I18nBreadcrumbResolver },
component: AdminWorkflowPageComponent, component: AdminWorkflowPageComponent,
data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' } data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' },
canActivate: [SiteAdministratorGuard]
}, },
{ {
path: 'curation-tasks', path: 'curation-tasks',
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: I18nBreadcrumbResolver },
component: AdminCurationTasksComponent, component: AdminCurationTasksComponent,
data: { title: 'admin.curation-tasks.title', breadcrumbKey: 'admin.curation-tasks' } data: { title: 'admin.curation-tasks.title', breadcrumbKey: 'admin.curation-tasks' },
canActivate: [SiteAdministratorGuard]
}, },
{ {
path: 'metadata-import', path: 'metadata-import',
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: I18nBreadcrumbResolver },
component: MetadataImportPageComponent, component: MetadataImportPageComponent,
data: { title: 'admin.metadata-import.title', breadcrumbKey: 'admin.metadata-import' } data: { title: 'admin.metadata-import.title', breadcrumbKey: 'admin.metadata-import' },
canActivate: [SiteAdministratorGuard]
}, },
{ {
path: 'batch-import', path: 'batch-import',
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: I18nBreadcrumbResolver },
component: BatchImportPageComponent, component: BatchImportPageComponent,
data: { title: 'admin.batch-import.title', breadcrumbKey: 'admin.batch-import' } data: { title: 'admin.batch-import.title', breadcrumbKey: 'admin.batch-import' },
}, canActivate: [SiteAdministratorGuard]
{
path: LDN_PATH,
children: [
{ path: '', pathMatch: 'full', redirectTo: 'services' },
{
path: 'services',
loadChildren: () => import('./admin-ldn-services/admin-ldn-services.module')
.then((m) => m.AdminLdnServicesModule),
}
],
canActivate: [NotifyInfoGuard]
}, },
{ {
path: 'system-wide-alert', path: 'system-wide-alert',
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: I18nBreadcrumbResolver },
loadChildren: () => import('../system-wide-alert/system-wide-alert.module').then((m) => m.SystemWideAlertModule), loadChildren: () => import('../system-wide-alert/system-wide-alert.module').then((m) => m.SystemWideAlertModule),
data: {title: 'admin.system-wide-alert.title', breadcrumbKey: 'admin.system-wide-alert'} data: {title: 'admin.system-wide-alert.title', breadcrumbKey: 'admin.system-wide-alert'},
canActivate: [SiteAdministratorGuard]
}, },
]), ])
], ],
providers: [ providers: [
I18nBreadcrumbResolver, I18nBreadcrumbResolver,

View File

@@ -133,3 +133,10 @@ export const SUBSCRIPTIONS_MODULE_PATH = 'subscriptions';
export function getSubscriptionsModuleRoute() { export function getSubscriptionsModuleRoute() {
return `/${SUBSCRIPTIONS_MODULE_PATH}`; return `/${SUBSCRIPTIONS_MODULE_PATH}`;
} }
export const EDIT_ITEM_PATH = 'edit-items';
export function getEditItemPageRoute() {
return `/${EDIT_ITEM_PATH}`;
}
export const CORRECTION_TYPE_PATH = 'corrections';

View File

@@ -3,9 +3,6 @@ import { RouterModule, NoPreloading } from '@angular/router';
import { AuthBlockingGuard } from './core/auth/auth-blocking.guard'; import { AuthBlockingGuard } from './core/auth/auth-blocking.guard';
import { AuthenticatedGuard } from './core/auth/authenticated.guard'; import { AuthenticatedGuard } from './core/auth/authenticated.guard';
import {
SiteAdministratorGuard
} from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
import { import {
ACCESS_CONTROL_MODULE_PATH, ACCESS_CONTROL_MODULE_PATH,
ADMIN_MODULE_PATH, ADMIN_MODULE_PATH,
@@ -157,7 +154,13 @@ import { ForgotPasswordCheckGuard } from './core/rest-property/forgot-password-c
path: ADMIN_MODULE_PATH, path: ADMIN_MODULE_PATH,
loadChildren: () => import('./admin/admin.module') loadChildren: () => import('./admin/admin.module')
.then((m) => m.AdminModule), .then((m) => m.AdminModule),
canActivate: [SiteAdministratorGuard, EndUserAgreementCurrentUserGuard] canActivate: [EndUserAgreementCurrentUserGuard]
},
{
path: NOTIFICATIONS_MODULE_PATH,
loadChildren: () => import('./admin/admin-notifications/admin-notifications.module')
.then((m) => m.AdminNotificationsModule),
canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard]
}, },
{ {
path: NOTIFICATIONS_MODULE_PATH, path: NOTIFICATIONS_MODULE_PATH,
@@ -251,7 +254,7 @@ import { ForgotPasswordCheckGuard } from './core/rest-property/forgot-password-c
.then((m) => m.SubscriptionsPageRoutingModule), .then((m) => m.SubscriptionsPageRoutingModule),
canActivate: [AuthenticatedGuard] canActivate: [AuthenticatedGuard]
}, },
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent }, { path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent }
] ]
} }
], { ], {

View File

@@ -2,7 +2,10 @@ import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model';
import { BreadcrumbsProviderService } from './breadcrumbsProviderService'; import { BreadcrumbsProviderService } from './breadcrumbsProviderService';
import { Observable, of as observableOf } from 'rxjs'; import { Observable, of as observableOf } from 'rxjs';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { map } from 'rxjs/operators';
import { getFirstCompletedRemoteData } from '../shared/operators';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { QualityAssuranceTopicDataService } from '../notifications/qa/topics/quality-assurance-topic-data.service';
@@ -16,6 +19,7 @@ export class QualityAssuranceBreadcrumbService implements BreadcrumbsProviderSer
private QUALITY_ASSURANCE_BREADCRUMB_KEY = 'admin.quality-assurance.breadcrumbs'; private QUALITY_ASSURANCE_BREADCRUMB_KEY = 'admin.quality-assurance.breadcrumbs';
constructor( constructor(
protected qualityAssuranceService: QualityAssuranceTopicDataService,
private translationService: TranslateService, private translationService: TranslateService,
) { ) {
@@ -28,14 +32,18 @@ export class QualityAssuranceBreadcrumbService implements BreadcrumbsProviderSer
* @param url The url to use as a link for this breadcrumb * @param url The url to use as a link for this breadcrumb
*/ */
getBreadcrumbs(key: string, url: string): Observable<Breadcrumb[]> { getBreadcrumbs(key: string, url: string): Observable<Breadcrumb[]> {
const args = key.split(':'); const sourceId = key.split(':')[0];
const sourceId = args[0]; const topicId = key.split(':')[2];
const topicId = args.length > 2 ? args[args.length - 1] : args[1];
if (topicId) { if (topicId) {
return observableOf( [new Breadcrumb(this.translationService.instant(this.QUALITY_ASSURANCE_BREADCRUMB_KEY), url), return this.qualityAssuranceService.getTopic(topicId).pipe(
getFirstCompletedRemoteData(),
map((topic) => {
return [new Breadcrumb(this.translationService.instant(this.QUALITY_ASSURANCE_BREADCRUMB_KEY), url),
new Breadcrumb(sourceId, `${url}${sourceId}`), new Breadcrumb(sourceId, `${url}${sourceId}`),
new Breadcrumb(topicId, undefined)]); new Breadcrumb(topicId.replace(/[!:]/g, '/'), undefined)];
})
);
} else { } else {
return observableOf([new Breadcrumb(this.translationService.instant(this.QUALITY_ASSURANCE_BREADCRUMB_KEY), url), return observableOf([new Breadcrumb(this.translationService.instant(this.QUALITY_ASSURANCE_BREADCRUMB_KEY), url),
new Breadcrumb(sourceId, `${url}${sourceId}`)]); new Breadcrumb(sourceId, `${url}${sourceId}`)]);

View File

@@ -185,19 +185,9 @@ import { FlatBrowseDefinition } from './shared/flat-browse-definition.model';
import { ValueListBrowseDefinition } from './shared/value-list-browse-definition.model'; import { ValueListBrowseDefinition } from './shared/value-list-browse-definition.model';
import { NonHierarchicalBrowseDefinition } from './shared/non-hierarchical-browse-definition'; import { NonHierarchicalBrowseDefinition } from './shared/non-hierarchical-browse-definition';
import { BulkAccessConditionOptions } from './config/models/bulk-access-condition-options.model'; import { BulkAccessConditionOptions } from './config/models/bulk-access-condition-options.model';
import { LdnServicesService } from '../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service'; import { CorrectionTypeDataService } from './submission/correctiontype-data.service';
import { LdnService } from '../admin/admin-ldn-services/ldn-services-model/ldn-services.model'; import { SuggestionTarget } from './suggestion-notifications/models/suggestion-target.model';
import { LdnItemfiltersService } from '../admin/admin-ldn-services/ldn-services-data/ldn-itemfilters-data.service'; import { SuggestionSource } from './suggestion-notifications/models/suggestion-source.model';
import { Itemfilter } from '../admin/admin-ldn-services/ldn-services-model/ldn-service-itemfilters';
import {
CoarNotifyConfigDataService
} from '../submission/sections/section-coar-notify/coar-notify-config-data.service';
import { SubmissionCoarNotifyConfig } from '../submission/sections/section-coar-notify/submission-coar-notify.config';
import { NotifyRequestsStatus } from '../item-page/simple/notify-requests-status/notify-requests-status.model';
import { NotifyRequestsStatusDataService } from './data/notify-services-status-data.service';
import { SuggestionTarget } from './notifications/models/suggestion-target.model';
import { SuggestionSource } from './notifications/models/suggestion-source.model';
/** /**
* When not in production, endpoint responses can be mocked for testing purposes * When not in production, endpoint responses can be mocked for testing purposes
@@ -321,10 +311,7 @@ const PROVIDERS = [
OrcidQueueDataService, OrcidQueueDataService,
OrcidHistoryDataService, OrcidHistoryDataService,
SupervisionOrderDataService, SupervisionOrderDataService,
LdnServicesService, CorrectionTypeDataService
LdnItemfiltersService,
CoarNotifyConfigDataService,
NotifyRequestsStatusDataService
]; ];
/** /**
@@ -405,11 +392,7 @@ export const models =
ItemRequest, ItemRequest,
BulkAccessConditionOptions, BulkAccessConditionOptions,
SuggestionTarget, SuggestionTarget,
SuggestionSource, SuggestionSource
LdnService,
Itemfilter,
SubmissionCoarNotifyConfig,
NotifyRequestsStatus,
]; ];
@NgModule({ @NgModule({

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { find, take } from 'rxjs/operators'; import { find, switchMap, take } from 'rxjs/operators';
import { ReplaceOperation } from 'fast-json-patch'; import { ReplaceOperation } from 'fast-json-patch';
import { HALEndpointService } from '../../../shared/hal-endpoint.service'; import { HALEndpointService } from '../../../shared/hal-endpoint.service';
@@ -25,6 +25,11 @@ import { SearchData, SearchDataImpl } from '../../../data/base/search-data';
import { DefaultChangeAnalyzer } from '../../../data/default-change-analyzer.service'; import { DefaultChangeAnalyzer } from '../../../data/default-change-analyzer.service';
import { hasValue } from '../../../../shared/empty.util'; import { hasValue } from '../../../../shared/empty.util';
import { DeleteByIDRequest, PostRequest } from '../../../data/request.models'; import { DeleteByIDRequest, PostRequest } from '../../../data/request.models';
import { HttpHeaders, HttpParams } from '@angular/common/http';
import { HttpOptions } from '../../../dspace-rest/dspace-rest.service';
import {
QualityAssuranceEventData
} from '../../../../notifications/qa/project-entry-import-modal/project-entry-import-modal.component';
/** /**
* The service handling all Quality Assurance topic REST requests. * The service handling all Quality Assurance topic REST requests.
@@ -210,4 +215,38 @@ export class QualityAssuranceEventDataService extends IdentifiableDataService<Qu
return this.rdbService.buildFromRequestUUID<QualityAssuranceEventObject>(requestId); return this.rdbService.buildFromRequestUUID<QualityAssuranceEventObject>(requestId);
} }
/**
* Perform a post on an endpoint related to correction type
* @param data the data to post
* @returns the RestResponse as an Observable
*/
postData(target: string, correctionType: string, related: string, reason: string): Observable<RemoteData<QualityAssuranceEventObject>> {
const requestId = this.requestService.generateRequestId();
const href$ = this.getBrowseEndpoint();
return href$.pipe(
switchMap((href: string) => {
const options: HttpOptions = Object.create({});
let headers = new HttpHeaders();
headers = headers.append('Content-Type', 'application/json');
options.headers = headers;
let params = new HttpParams();
params = params.append('target', target)
.append('correctionType', correctionType);
options.params = params;
const request = new PostRequest(requestId, href, {'reason': reason} , options);
if (hasValue(this.responseMsToLive)) {
request.responseMsToLive = this.responseMsToLive;
}
this.requestService.send(request);
return this.rdbService.buildFromRequestUUID<QualityAssuranceEventObject>(requestId);
})
);
}
public deleteQAEvent(qaEvent: QualityAssuranceEventData): Observable<RemoteData<NoContent>> {
return this.deleteData.delete(qaEvent.id);
}
} }

View File

@@ -28,6 +28,8 @@ export interface SourceQualityAssuranceEventMessageObject {
*/ */
type: string; type: string;
reason: string;
/** /**
* The value suggested by Notifications * The value suggested by Notifications
*/ */

View File

@@ -16,7 +16,7 @@ import { PaginatedList } from '../../../data/paginated-list.model';
import { FindListOptions } from '../../../data/find-list-options.model'; import { FindListOptions } from '../../../data/find-list-options.model';
import { IdentifiableDataService } from '../../../data/base/identifiable-data.service'; import { IdentifiableDataService } from '../../../data/base/identifiable-data.service';
import { FindAllData, FindAllDataImpl } from '../../../data/base/find-all-data'; import { FindAllData, FindAllDataImpl } from '../../../data/base/find-all-data';
import { SearchData, SearchDataImpl } from 'src/app/core/data/base/search-data'; import { SearchData, SearchDataImpl } from '../../../data/base/search-data';
/** /**
* The service handling all Quality Assurance source REST requests. * The service handling all Quality Assurance source REST requests.

View File

@@ -81,8 +81,9 @@ describe('QualityAssuranceTopicDataService', () => {
notificationsService notificationsService
); );
spyOn((service as any).searchData, 'searchBy').and.callThrough(); spyOn((service as any).findAllData, 'findAll').and.callThrough();
spyOn((service as any), 'findById').and.callThrough(); spyOn((service as any), 'findById').and.callThrough();
spyOn((service as any).searchData, 'searchBy').and.callThrough();
}); });
describe('searchTopicsByTarget', () => { describe('searchTopicsByTarget', () => {
@@ -110,31 +111,6 @@ describe('QualityAssuranceTopicDataService', () => {
}); });
}); });
describe('searchTopicsBySource', () => {
it('should call searchData.searchBy with the correct parameters', () => {
const options = { elementsPerPage: 10 };
const useCachedVersionIfAvailable = true;
const reRequestOnStale = true;
service.searchTopicsBySource(options, useCachedVersionIfAvailable, reRequestOnStale);
expect((service as any).searchData.searchBy).toHaveBeenCalledWith(
'bySource',
options,
useCachedVersionIfAvailable,
reRequestOnStale,
);
});
it('should return a RemoteData<PaginatedList<QualityAssuranceTopicObject>> for the object with the given URL', () => {
const result = service.searchTopicsBySource();
const expected = cold('(a)', {
a: paginatedListRD
});
expect(result).toBeObservable(expected);
});
});
describe('getTopic', () => { describe('getTopic', () => {
it('should call findByHref', (done) => { it('should call findByHref', (done) => {
service.getTopic(qualityAssuranceTopicObjectMorePid.id).subscribe( service.getTopic(qualityAssuranceTopicObjectMorePid.id).subscribe(
@@ -153,4 +129,5 @@ describe('QualityAssuranceTopicDataService', () => {
expect(result).toBeObservable(expected); expect(result).toBeObservable(expected);
}); });
}); });
}); });

View File

@@ -15,7 +15,7 @@ import { FindListOptions } from '../../../data/find-list-options.model';
import { IdentifiableDataService } from '../../../data/base/identifiable-data.service'; import { IdentifiableDataService } from '../../../data/base/identifiable-data.service';
import { dataService } from '../../../data/base/data-service.decorator'; import { dataService } from '../../../data/base/data-service.decorator';
import { QUALITY_ASSURANCE_TOPIC_OBJECT } from '../models/quality-assurance-topic-object.resource-type'; import { QUALITY_ASSURANCE_TOPIC_OBJECT } from '../models/quality-assurance-topic-object.resource-type';
import { SearchData, SearchDataImpl } from '../../../../core/data/base/search-data'; import { SearchData, SearchDataImpl } from '../../../data/base/search-data';
import { FindAllData, FindAllDataImpl } from '../../../data/base/find-all-data'; import { FindAllData, FindAllDataImpl } from '../../../data/base/find-all-data';
/** /**

View File

@@ -0,0 +1,89 @@
import { Injectable } from '@angular/core';
import { dataService } from '../data/base/data-service.decorator';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { RequestService } from '../data/request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { IdentifiableDataService } from '../data/base/identifiable-data.service';
import { SearchDataImpl } from '../data/base/search-data';
import { CorrectionType } from './models/correctiontype.model';
import { Observable, map } from 'rxjs';
import { RemoteData } from '../data/remote-data';
import { PaginatedList } from '../data/paginated-list.model';
import { FindListOptions } from '../data/find-list-options.model';
import { RequestParam } from '../cache/models/request-param.model';
import { getAllSucceededRemoteDataPayload, getPaginatedListPayload } from '../shared/operators';
/**
* A service that provides methods to make REST requests with correctiontypes endpoint.
*/
@Injectable()
@dataService(CorrectionType.type)
export class CorrectionTypeDataService extends IdentifiableDataService<CorrectionType> {
protected linkPath = 'correctiontypes';
protected searchByTopic = 'findByTopic';
protected searchFindByItem = 'findByItem';
private searchData: SearchDataImpl<CorrectionType>;
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
) {
super('correctiontypes', requestService, rdbService, objectCache, halService);
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
}
/**
* Get the correction type by id
* @param id the id of the correction type
* @param useCachedVersionIfAvailable use the cached version if available
* @param reRequestOnStale re-request on stale
* @returns {Observable<RemoteData<CorrectionType>>} the correction type
*/
getCorrectionTypeById(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true): Observable<RemoteData<CorrectionType>> {
return this.findById(id, useCachedVersionIfAvailable, reRequestOnStale);
}
/**
* Search for the correction types for the item
* @param itemUuid the uuid of the item
* @param useCachedVersionIfAvailable use the cached version if available
* @returns the list of correction types for the item
*/
findByItem(itemUuid: string, useCachedVersionIfAvailable): Observable<RemoteData<PaginatedList<CorrectionType>>> {
const options = new FindListOptions();
options.searchParams = [new RequestParam('uuid', itemUuid)];
return this.searchData.searchBy(this.searchFindByItem, options, useCachedVersionIfAvailable);
}
/**
* Find the correction type for the topic
* @param topic the topic of the correction type to search for
* @param useCachedVersionIfAvailable use the cached version if available
* @param reRequestOnStale re-request on stale
* @returns the correction type for the topic
*/
findByTopic(topic: string, useCachedVersionIfAvailable = true, reRequestOnStale = true): Observable<CorrectionType> {
const options = new FindListOptions();
options.searchParams = [
{
fieldName: 'topic',
fieldValue: topic,
},
];
return this.searchData.searchBy(this.searchByTopic, options, useCachedVersionIfAvailable, reRequestOnStale).pipe(
getAllSucceededRemoteDataPayload(),
getPaginatedListPayload(),
map((list: CorrectionType[]) => {
return list[0];
})
);
}
}

View File

@@ -0,0 +1,49 @@
import { autoserialize, deserialize } from 'cerialize';
import { typedObject } from '../../cache/builders/build-decorators';
import { CacheableObject } from '../../cache/cacheable-object.model';
import { ResourceType } from '../../shared/resource-type';
import { excludeFromEquals } from '../../utilities/equals.decorators';
import { HALLink } from '../../shared/hal-link.model';
@typedObject
/**
* Represents a correction type. It extends the CacheableObject.
* The correction type represents a type of correction that can be applied to a submission.
*/
export class CorrectionType extends CacheableObject {
static type = new ResourceType('correctiontype');
/**
* The object type
*/
@excludeFromEquals
@autoserialize
type: ResourceType;
@autoserialize
/**
* The unique identifier for the correction type mode.
*/
id: string;
@autoserialize
/**
* The topic of the correction type mode.
*/
topic: string;
@autoserialize
/**
* The discovery configuration for the correction type mode.
*/
discoveryConfiguration: string;
@autoserialize
/**
* The form used for creating a correction type.
*/
creationForm: string;
@deserialize
/**
* Represents the links associated with the correction type mode.
*/
_links: {
self: HALLink;
};
}

View File

@@ -6,7 +6,10 @@
<ds-alert [type]="AlertTypeEnum.Warning"> <ds-alert [type]="AlertTypeEnum.Warning">
<div class="d-flex justify-content-between flex-wrap"> <div class="d-flex justify-content-between flex-wrap">
<span class="align-self-center">{{'item.alerts.withdrawn' | translate}}</span> <span class="align-self-center">{{'item.alerts.withdrawn' | translate}}</span>
<a routerLink="/home" class="btn btn-primary btn-sm">{{"404.link.home-page" | translate}}</a> <div class="gap-2 d-flex">
<a routerLink="/home" class="btn btn-primary btn-sm">{{"404.link.home-page" | translate}}</a>
<a *ngIf="showReinstateButton$() | async" class="btn btn-primary btn-sm" (click)="openReinstateModal()">{{ 'item.alerts.reinstate-request' | translate}}</a>
</div>
</div> </div>
</ds-alert> </ds-alert>
</div> </div>

View File

@@ -4,16 +4,41 @@ import { TranslateModule } from '@ngx-translate/core';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { DsoWithdrawnReinstateModalService, REQUEST_REINSTATE } from '../../shared/dso-page/dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service';
import { CorrectionTypeDataService } from '../../core/submission/correctiontype-data.service';
import { TestScheduler } from 'rxjs/testing';
import { CorrectionType } from '../../core/submission/models/correctiontype.model';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { createPaginatedList } from '../../shared/testing/utils.test';
import { of } from 'rxjs';
describe('ItemAlertsComponent', () => { describe('ItemAlertsComponent', () => {
let component: ItemAlertsComponent; let component: ItemAlertsComponent;
let fixture: ComponentFixture<ItemAlertsComponent>; let fixture: ComponentFixture<ItemAlertsComponent>;
let item: Item; let item: Item;
let authorizationService;
let dsoWithdrawnReinstateModalService;
let correctionTypeDataService;
let testScheduler: TestScheduler;
const itemMock = Object.assign(new Item(), {
uuid: 'item-uuid',
id: 'item-uuid',
});
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
authorizationService = jasmine.createSpyObj('authorizationService', ['isAuthorized']);
dsoWithdrawnReinstateModalService = jasmine.createSpyObj('dsoWithdrawnReinstateModalService', ['openCreateWithdrawnReinstateModal']);
correctionTypeDataService = jasmine.createSpyObj('correctionTypeDataService', ['findByItem']);
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ItemAlertsComponent], declarations: [ItemAlertsComponent],
imports: [TranslateModule.forRoot()], imports: [TranslateModule.forRoot()],
providers: [
{ provide: AuthorizationDataService, useValue: authorizationService },
{ provide: DsoWithdrawnReinstateModalService, useValue: dsoWithdrawnReinstateModalService },
{ provide: CorrectionTypeDataService, useValue: correctionTypeDataService }
],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}) })
.compileComponents(); .compileComponents();
@@ -21,7 +46,9 @@ describe('ItemAlertsComponent', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(ItemAlertsComponent); fixture = TestBed.createComponent(ItemAlertsComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
component.item = itemMock;
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -61,6 +88,7 @@ describe('ItemAlertsComponent', () => {
isWithdrawn: true isWithdrawn: true
}); });
component.item = item; component.item = item;
(correctionTypeDataService.findByItem).and.returnValue(createSuccessfulRemoteDataObject$([]));
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -76,6 +104,7 @@ describe('ItemAlertsComponent', () => {
isWithdrawn: false isWithdrawn: false
}); });
component.item = item; component.item = item;
(correctionTypeDataService.findByItem).and.returnValue(createSuccessfulRemoteDataObject$([]));
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -84,4 +113,43 @@ describe('ItemAlertsComponent', () => {
expect(privateWarning).toBeNull(); expect(privateWarning).toBeNull();
}); });
}); });
describe('when the item is reinstated', () => {
const correctionType = Object.assign(new CorrectionType(), {
topic: REQUEST_REINSTATE
});
const correctionRD = createSuccessfulRemoteDataObject(createPaginatedList([correctionType]));
beforeEach(() => {
item = itemMock;
component.item = item;
(correctionTypeDataService.findByItem).and.returnValue(of(correctionRD));
testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
fixture.detectChanges();
});
it('should return true when user is not an admin and there is at least one correction with topic REQUEST_REINSTATE', () => {
testScheduler.run(({ cold, expectObservable }) => {
const isAdminMarble = 'a';
const correctionMarble = 'b';
const expectedMarble = 'c';
const isAdminValues = { a: false };
const correctionValues = { b: correctionRD };
const expectedValues = { c: true };
const isAdmin$ = cold(isAdminMarble, isAdminValues);
const correction$ = cold(correctionMarble, correctionValues);
(authorizationService.isAuthorized).and.returnValue(isAdmin$);
(correctionTypeDataService.findByItem).and.returnValue(correction$);
expectObservable(component.showReinstateButton$()).toBe(expectedMarble, expectedValues);
});
});
});
}); });

View File

@@ -1,6 +1,12 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { AlertType } from '../../shared/alert/alert-type'; import { AlertType } from '../../shared/alert/alert-type';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import { Observable, combineLatest, map } from 'rxjs';
import { DsoWithdrawnReinstateModalService, REQUEST_REINSTATE } from '../../shared/dso-page/dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service';
import { CorrectionTypeDataService } from '../../core/submission/correctiontype-data.service';
import { getFirstCompletedRemoteData, getPaginatedListPayload, getRemoteDataPayload } from 'src/app/core/shared/operators';
@Component({ @Component({
selector: 'ds-item-alerts', selector: 'ds-item-alerts',
@@ -21,4 +27,37 @@ export class ItemAlertsComponent {
* @type {AlertType} * @type {AlertType}
*/ */
public AlertTypeEnum = AlertType; public AlertTypeEnum = AlertType;
constructor(
private authService: AuthorizationDataService,
private dsoWithdrawnReinstateModalService: DsoWithdrawnReinstateModalService,
private correctionTypeDataService: CorrectionTypeDataService
) {
}
/**
* Determines whether to show the reinstate button.
* The button is shown if the user is not an admin and the item has a reinstate request.
* @returns An Observable that emits a boolean value indicating whether to show the reinstate button.
*/
showReinstateButton$(): Observable<boolean> {
const correction$ = this.correctionTypeDataService.findByItem(this.item.uuid, true).pipe(
getFirstCompletedRemoteData(),
getRemoteDataPayload(),
getPaginatedListPayload()
);
const isAdmin$ = this.authService.isAuthorized(FeatureID.AdministratorOf);
return combineLatest([isAdmin$, correction$]).pipe(
map(([isAdmin, correction]) => {
return !isAdmin && correction.some((correctionType) => correctionType.topic === REQUEST_REINSTATE);
}
));
}
/**
* Opens the reinstate modal for the item.
*/
openReinstateModal() {
this.dsoWithdrawnReinstateModalService.openCreateWithdrawnReinstateModal(this.item, 'request-reinstate', this.item.isArchived);
}
} }

View File

@@ -30,11 +30,15 @@ import { RelatedItemsComponent } from './simple/related-items/related-items-comp
import { import {
ThemedMetadataRepresentationListComponent ThemedMetadataRepresentationListComponent
} from './simple/metadata-representation-list/themed-metadata-representation-list.component'; } from './simple/metadata-representation-list/themed-metadata-representation-list.component';
import {
ItemWithdrawnReinstateModalComponent
} from '../shared/correction-suggestion/withdrawn-reinstate-modal.component';
import { ItemPageImgFieldComponent } from './simple/field-components/specific-field/img/item-page-img-field.component'; import { ItemPageImgFieldComponent } from './simple/field-components/specific-field/img/item-page-img-field.component';
const ENTRY_COMPONENTS = [ const ENTRY_COMPONENTS = [
ItemVersionsDeleteModalComponent, ItemVersionsDeleteModalComponent,
ItemVersionsSummaryModalComponent, ItemVersionsSummaryModalComponent,
ItemWithdrawnReinstateModalComponent
]; ];

View File

@@ -1,13 +1,21 @@
<ng-container *ngIf="(getQualityAssuranceSources$() | async)?.length > 0"> <ng-container *ngIf="(sources$ | async) as sources">
<ng-container *ngFor="let source of (getQualityAssuranceSources$() | async)"> <ng-container *ngFor="let source of sources">
<div class="alert alert-info d-flex flex-row" *ngIf="source.totalEvents > 0"> <div class="alert alert-info d-flex flex-row" *ngIf="source.totalEvents > 0">
<img class="source-logo" src="assets/images/qa-{{(source.id | dsSplit: ':')[0]}}-logo.png" alt="{{source.id}} logo"> <div class="source-logo-container">
<img class="source-logo"
src="assets/images/qa-{{(source.id | dsSplit: ':')[0]}}-logo.png"
alt="{{source.id}} logo"
onerror="this.src='assets/images/dspace-logo.svg'">
</div>
<div class="w-100 d-flex justify-content-between"> <div class="w-100 d-flex justify-content-between">
<div class="pl-4 align-self-center">{{'item.qa-event-notification.check.notification-info' | translate : {num: <div class="pl-4 align-self-center">
source.totalEvents } }} </div> {{'item.qa-event-notification.check.notification-info' | translate : {num: source.totalEvents } }}
</div>
<button [routerLink]="[ getQualityAssuranceRoute(), (source.id | dsSplit: ':')[0], 'target', item.id]" <button [routerLink]="[ getQualityAssuranceRoute(), (source.id | dsSplit: ':')[0], 'target', item.id]"
class="btn btn-primary align-self-center">{{'item.qa-event-notification-info.check.button' | translate [queryParams]="{ forward: true }"
}}</button> class="btn btn-primary align-self-center">
{{'item.qa-event-notification-info.check.button' | translate}}
</button>
</div> </div>
</div> </div>
</ng-container> </ng-container>

View File

@@ -1,8 +1,13 @@
.source-logo { .source-logo {
max-height: var(--ds-header-logo-height); max-height: var(--ds-header-logo-height);
} }
.source-logo-container {
width: var(--ds-qa-logo-width);
display: flex;
justify-content: center;
}
.sections-gap { .sections-gap {
gap: 1rem; gap: 1rem;
} }

View File

@@ -1,34 +1,40 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { QaEventNotificationComponent } from './qa-event-notification.component'; import { QaEventNotificationComponent } from './qa-event-notification.component';
import { QualityAssuranceSourceDataService } from '../../../core/notifications/qa/source/quality-assurance-source-data.service'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { createPaginatedList } from '../../../shared/testing/utils.test'; import { createPaginatedList } from '../../../shared/testing/utils.test';
import { QualityAssuranceSourceObject } from '../../../core/notifications/qa/models/quality-assurance-source.model'; import { QualityAssuranceSourceObject } from '../../../core/notifications/qa/models/quality-assurance-source.model';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { TranslateModule } from '@ngx-translate/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { SplitPipe } from '../../../shared/utils/split.pipe'; import { TranslateModule } from '@ngx-translate/core';
import { QualityAssuranceSourceDataService } from '../../../core/notifications/qa/source/quality-assurance-source-data.service';
import { RequestService } from '../../../core/data/request.service'; import { RequestService } from '../../../core/data/request.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { provideMockStore } from '@ngrx/store/testing'; import { provideMockStore } from '@ngrx/store/testing';
import { HALEndpointService } from '../../../core/shared/hal-endpoint.service'; import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub';
import { of } from 'rxjs';
import { By } from '@angular/platform-browser';
import { SplitPipe } from 'src/app/shared/utils/split.pipe';
describe('QaEventNotificationComponent', () => { describe('QaEventNotificationComponent', () => {
let component: QaEventNotificationComponent; let component: QaEventNotificationComponent;
let fixture: ComponentFixture<QaEventNotificationComponent>; let fixture: ComponentFixture<QaEventNotificationComponent>;
let qualityAssuranceSourceDataServiceStub: any; let qualityAssuranceSourceDataServiceStub: any;
const obj = createSuccessfulRemoteDataObject$(createPaginatedList([new QualityAssuranceSourceObject()])); const obj = Object.assign(new QualityAssuranceSourceObject(), {
const item = Object.assign({ uuid: '1234' }); id: 'sourceName:target',
source: 'sourceName',
target: 'target',
totalEvents: 1
});
const objPL = createSuccessfulRemoteDataObject$(createPaginatedList([obj]));
const item = Object.assign({ uuid: '1234' });
beforeEach(async () => { beforeEach(async () => {
qualityAssuranceSourceDataServiceStub = { qualityAssuranceSourceDataServiceStub = {
getSourcesByTarget: () => obj getSourcesByTarget: () => objPL
}; };
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [CommonModule, TranslateModule.forRoot()], imports: [CommonModule, TranslateModule.forRoot()],
@@ -37,22 +43,31 @@ describe('QaEventNotificationComponent', () => {
{ provide: QualityAssuranceSourceDataService, useValue: qualityAssuranceSourceDataServiceStub }, { provide: QualityAssuranceSourceDataService, useValue: qualityAssuranceSourceDataServiceStub },
{ provide: RequestService, useValue: {} }, { provide: RequestService, useValue: {} },
{ provide: NotificationsService, useValue: {} }, { provide: NotificationsService, useValue: {} },
{ provide: HALEndpointService, useValue: new HALEndpointServiceStub('test')}, { provide: HALEndpointService, useValue: new HALEndpointServiceStub('test') },
ObjectCacheService, ObjectCacheService,
RemoteDataBuildService, RemoteDataBuildService,
provideMockStore({}) provideMockStore({})
], ],
schemas: [NO_ERRORS_SCHEMA]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(QaEventNotificationComponent); fixture = TestBed.createComponent(QaEventNotificationComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
component.item = item; component.item = item;
component.sources$ = of([obj]);
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it('should create', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should display sources if present', () => {
const alertElements = fixture.debugElement.queryAll(By.css('.alert'));
expect(alertElements.length).toBe(1);
});
it('should return the quality assurance route when getQualityAssuranceRoute is called', () => {
const route = component.getQualityAssuranceRoute();
expect(route).toBe('/notifications/quality-assurance');
});
}); });

View File

@@ -1,15 +1,15 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { getFirstCompletedRemoteData, getPaginatedListPayload, getRemoteDataPayload } from '../../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { Observable, filter } from 'rxjs'; import { Observable } from 'rxjs';
import { AlertType } from '../../../shared/alert/alert-type';
import { FindListOptions } from '../../../core/data/find-list-options.model'; import { FindListOptions } from '../../../core/data/find-list-options.model';
import { RequestParam } from '../../../core/cache/models/request-param.model'; import { RequestParam } from '../../../core/cache/models/request-param.model';
import { getNotificatioQualityAssuranceRoute } from '../../../admin/admin-routing-paths';
import { QualityAssuranceSourceDataService } from '../../../core/notifications/qa/source/quality-assurance-source-data.service'; import { QualityAssuranceSourceDataService } from '../../../core/notifications/qa/source/quality-assurance-source-data.service';
import { QualityAssuranceSourceObject } from '../../../core/notifications/qa/models/quality-assurance-source.model'; import { QualityAssuranceSourceObject } from '../../../core/notifications/qa/models/quality-assurance-source.model';
import { PaginatedList } from 'src/app/core/data/paginated-list.model'; import { catchError, map } from 'rxjs/operators';
import { hasValue } from 'src/app/shared/empty.util'; import { RemoteData } from '../../../core/data/remote-data';
import { getNotificatioQualityAssuranceRoute } from '../../../admin/admin-routing-paths';
import { PaginatedList } from '../../../core/data/paginated-list.model';
@Component({ @Component({
selector: 'ds-qa-event-notification', selector: 'ds-qa-event-notification',
@@ -21,22 +21,29 @@ import { hasValue } from 'src/app/shared/empty.util';
/** /**
* Component for displaying quality assurance event notifications for an item. * Component for displaying quality assurance event notifications for an item.
*/ */
export class QaEventNotificationComponent { export class QaEventNotificationComponent implements OnChanges {
/** /**
* The item to display quality assurance event notifications for. * The item to display quality assurance event notifications for.
*/ */
@Input() item: Item; @Input() item: Item;
/** /**
* The type of alert to display for the notification. * An observable that emits an array of QualityAssuranceSourceObject.
*/ */
AlertTypeInfo = AlertType.Info; sources$: Observable<QualityAssuranceSourceObject[]>;
constructor( constructor(
private qualityAssuranceSourceDataService: QualityAssuranceSourceDataService, private qualityAssuranceSourceDataService: QualityAssuranceSourceDataService,
) { } ) {}
/**
* Detect changes to the item input and update the sources$ observable.
*/
ngOnChanges(changes: SimpleChanges): void {
if (changes.item && changes.item.currentValue.uuid !== changes.item.previousValue?.uuid) {
this.sources$ = this.getQualityAssuranceSources$();
}
}
/** /**
* Returns an Observable of QualityAssuranceSourceObject[] for the current item. * Returns an Observable of QualityAssuranceSourceObject[] for the current item.
* @returns An Observable of QualityAssuranceSourceObject[] for the current item. * @returns An Observable of QualityAssuranceSourceObject[] for the current item.
@@ -46,12 +53,16 @@ export class QaEventNotificationComponent {
const findListTopicOptions: FindListOptions = { const findListTopicOptions: FindListOptions = {
searchParams: [new RequestParam('target', this.item.uuid)] searchParams: [new RequestParam('target', this.item.uuid)]
}; };
return this.qualityAssuranceSourceDataService.getSourcesByTarget(findListTopicOptions) return this.qualityAssuranceSourceDataService.getSourcesByTarget(findListTopicOptions, false)
.pipe( .pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
getRemoteDataPayload(), map((data: RemoteData<PaginatedList<QualityAssuranceSourceObject>>) => {
filter((pl: PaginatedList<QualityAssuranceSourceObject>) => hasValue(pl)), if (data.hasSucceeded) {
getPaginatedListPayload(), return data.payload.page;
}
return [];
}),
catchError(() => [])
); );
} }

View File

@@ -1,7 +1,7 @@
<div class="container"> <div class="container">
<ds-suggestions-notification></ds-suggestions-notification>
<ds-my-dspace-qa-events-notifications></ds-my-dspace-qa-events-notifications> <ds-my-dspace-qa-events-notifications></ds-my-dspace-qa-events-notifications>
<ds-my-dspace-new-submission *dsShowOnlyForRole="[roleTypeEnum.Submitter]"></ds-my-dspace-new-submission> <ds-my-dspace-new-submission *dsShowOnlyForRole="[roleTypeEnum.Submitter]"></ds-my-dspace-new-submission>
<ds-suggestions-notification></ds-suggestions-notification>
</div> </div>
<ds-themed-search *ngIf="configuration && context" <ds-themed-search *ngIf="configuration && context"

View File

@@ -15,8 +15,10 @@ import { MyDSpaceNewExternalDropdownComponent } from './my-dspace-new-submission
import { ThemedMyDSpacePageComponent } from './themed-my-dspace-page.component'; import { ThemedMyDSpacePageComponent } from './themed-my-dspace-page.component';
import { SearchModule } from '../shared/search/search.module'; import { SearchModule } from '../shared/search/search.module';
import { UploadModule } from '../shared/upload/upload.module'; import { UploadModule } from '../shared/upload/upload.module';
import {
MyDspaceQaEventsNotificationsComponent
} from './my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component';
import { NotificationsModule } from '../notifications/notifications.module'; import { NotificationsModule } from '../notifications/notifications.module';
import { MyDspaceQaEventsNotificationsComponent } from './my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component';
const DECLARATIONS = [ const DECLARATIONS = [
MyDSpacePageComponent, MyDSpacePageComponent,
@@ -25,19 +27,19 @@ const DECLARATIONS = [
CollectionSelectorComponent, CollectionSelectorComponent,
MyDSpaceNewSubmissionDropdownComponent, MyDSpaceNewSubmissionDropdownComponent,
MyDSpaceNewExternalDropdownComponent, MyDSpaceNewExternalDropdownComponent,
MyDspaceQaEventsNotificationsComponent, MyDspaceQaEventsNotificationsComponent
]; ];
@NgModule({ @NgModule({
imports: [ imports: [
CommonModule, CommonModule,
SharedModule, SharedModule,
SearchModule, SearchModule,
MyDspacePageRoutingModule, MyDspacePageRoutingModule,
MyDspaceSearchModule.withEntryComponents(), MyDspaceSearchModule.withEntryComponents(),
UploadModule, UploadModule,
NotificationsModule, NotificationsModule
], ],
declarations: DECLARATIONS, declarations: DECLARATIONS,
providers: [ providers: [
MyDSpaceGuard, MyDSpaceGuard,

View File

@@ -4,20 +4,21 @@
class="alert alert-info d-flex flex-row" class="alert alert-info d-flex flex-row"
*ngIf="source.totalEvents > 0" *ngIf="source.totalEvents > 0"
> >
<div class="source-logo-container">
<img <img
class="source-logo" class="source-logo"
src="assets/images/qa-{{ source.id }}-logo.png" src="assets/images/qa-{{ source.id }}-logo.png"
onerror="this.src='assets/images/dspace-logo.svg'"
alt="{{ source.id }} logo" alt="{{ source.id }} logo"
/> />
</div>
<div class="w-100 d-flex justify-content-between"> <div class="w-100 d-flex justify-content-between">
<div class="pl-4 align-self-center"> <div class="pl-4 align-self-center">
{{ {{ "mydspace.qa-event-notification.check.notification-info" | translate : { num: source.totalEvents } }}
"mydspace.qa-event-notification.check.notification-info"
| translate : { num: source.totalEvents }
}}
</div> </div>
<button <button
[routerLink]="[getQualityAssuranceRoute(), source.id]" [routerLink]="[getQualityAssuranceRoute(), source.id]"
[queryParams]="{ forward: true }"
class="btn btn-primary align-self-center" class="btn btn-primary align-self-center"
> >
{{ "mydspace.qa-event-notification-info.check.button" | translate }} {{ "mydspace.qa-event-notification-info.check.button" | translate }}

View File

@@ -1,8 +1,13 @@
.source-logo { .source-logo {
max-height: var(--ds-header-logo-height); max-height: var(--ds-header-logo-height);
} }
.source-logo-container {
width: var(--ds-qa-logo-width);
display: flex;
justify-content: center;
}
.sections-gap { .sections-gap {
gap: 1rem; gap: 1rem;
} }

View File

@@ -26,23 +26,30 @@ import { QualityAssuranceSourceService } from './qa/source/quality-assurance-sou
import { import {
QualityAssuranceSourceDataService QualityAssuranceSourceDataService
} from '../core/notifications/qa/source/quality-assurance-source-data.service'; } from '../core/notifications/qa/source/quality-assurance-source-data.service';
import { EPersonDataComponent } from './qa/events/ePerson-data/ePerson-data.component';
import { SuggestionsPopupComponent } from './suggestions-popup/suggestions-popup.component'; import { PublicationClaimComponent } from '../suggestion-notifications/suggestion-targets/publication-claim/publication-claim.component';
import { SuggestionSourceDataService } from '../core/notifications/source/suggestion-source-data.service'; import { SuggestionActionsComponent } from '../suggestion-notifications/suggestion-actions/suggestion-actions.component';
import { SuggestionTargetDataService } from '../core/notifications/target/suggestion-target-data.service'; import {
import { SuggestionsDataService } from '../core/notifications/suggestions-data.service'; SuggestionListElementComponent
import { PublicationClaimComponent } from '../notifications/suggestion-targets/publication-claim/publication-claim.component'; } from '../suggestion-notifications/suggestion-list-element/suggestion-list-element.component';
import { SuggestionsNotificationComponent } from './suggestions-notification/suggestions-notification.component';
import { SuggestionsService } from './suggestions.service';
import { SuggestionTargetsStateService } from './suggestion-targets/suggestion-targets.state.service';
import { SuggestionActionsComponent } from './suggestion-actions/suggestion-actions.component';
import { SuggestionListElementComponent } from './suggestion-list-element/suggestion-list-element.component';
import { import {
SuggestionEvidencesComponent SuggestionEvidencesComponent
} from './suggestion-list-element/suggestion-evidences/suggestion-evidences.component'; } from '../suggestion-notifications/suggestion-list-element/suggestion-evidences/suggestion-evidences.component';
import { SuggestionsPopupComponent } from '../suggestion-notifications/suggestions-popup/suggestions-popup.component';
import {
SuggestionsNotificationComponent
} from '../suggestion-notifications/suggestions-notification/suggestions-notification.component';
import { SuggestionsService } from '../suggestion-notifications/suggestions.service';
import { SuggestionsDataService } from '../core/suggestion-notifications/suggestions-data.service';
import {
SuggestionSourceDataService
} from '../core/suggestion-notifications/source/suggestion-source-data.service';
import {
SuggestionTargetDataService
} from '../core/suggestion-notifications/target/suggestion-target-data.service';
import {
SuggestionTargetsStateService
} from '../suggestion-notifications/suggestion-targets/suggestion-targets.state.service';
const MODULES = [ const MODULES = [
@@ -59,6 +66,7 @@ const COMPONENTS = [
QualityAssuranceTopicsComponent, QualityAssuranceTopicsComponent,
QualityAssuranceEventsComponent, QualityAssuranceEventsComponent,
QualityAssuranceSourceComponent, QualityAssuranceSourceComponent,
EPersonDataComponent,
PublicationClaimComponent, PublicationClaimComponent,
SuggestionActionsComponent, SuggestionActionsComponent,
SuggestionListElementComponent, SuggestionListElementComponent,
@@ -94,7 +102,7 @@ const PROVIDERS = [
declarations: [ declarations: [
...COMPONENTS, ...COMPONENTS,
...DIRECTIVES, ...DIRECTIVES,
...ENTRY_COMPONENTS ...ENTRY_COMPONENTS,
], ],
providers: [ providers: [
...PROVIDERS ...PROVIDERS
@@ -104,7 +112,7 @@ const PROVIDERS = [
], ],
exports: [ exports: [
...COMPONENTS, ...COMPONENTS,
...DIRECTIVES ...DIRECTIVES,
] ]
}) })

View File

@@ -0,0 +1,10 @@
<ng-container *ngIf="ePersonId">
<ng-container *ngIf="getEPersonData$() | async as ePersonData">
<ng-container *ngFor="let property of properties">
<span *ngIf="ePersonData[property]">
{{ ePersonData[property] }}
</span>
<br>
</ng-container>
</ng-container>
</ng-container>

View File

@@ -0,0 +1,58 @@
/* tslint:disable:no-unused-variable */
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { EPersonDataComponent } from './ePerson-data.component';
import { EPersonDataService } from './../../../../core/eperson/eperson-data.service';
import { EPerson } from 'src/app/core/eperson/models/eperson.model';
import { createSuccessfulRemoteDataObject$ } from 'src/app/shared/remote-data.utils';
describe('EPersonDataComponent', () => {
let component: EPersonDataComponent;
let fixture: ComponentFixture<EPersonDataComponent>;
let ePersonDataService = jasmine.createSpyObj('EPersonDataService', ['findById']);
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ EPersonDataComponent ],
providers: [ {
provide: EPersonDataService,
useValue: ePersonDataService
} ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EPersonDataComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should retrieve EPerson data when ePersonId is provided', () => {
const ePersonId = '123';
const ePersonData = Object.assign(new EPerson(), {
id: ePersonId,
email: 'john.doe@domain.com',
metadata: [
{
key: 'eperson.firstname',
value: 'John'
},
{
key: 'eperson.lastname',
value: 'Doe'
}
]
});
const ePersonDataRD$ = createSuccessfulRemoteDataObject$(ePersonData);
ePersonDataService.findById.and.returnValue(ePersonDataRD$);
component.ePersonId = ePersonId;
component.getEPersonData$();
fixture.detectChanges();
expect(ePersonDataService.findById).toHaveBeenCalledWith(ePersonId, true);
});
});

View File

@@ -0,0 +1,45 @@
import { Component, Input } from '@angular/core';
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../../../../core/shared/operators';
import { Observable } from 'rxjs';
import { EPerson } from '../../../../core/eperson/models/eperson.model';
@Component({
selector: 'ds-eperson-data',
templateUrl: './ePerson-data.component.html',
styleUrls: ['./ePerson-data.component.scss']
})
/**
* Represents the component for displaying ePerson data.
*/
export class EPersonDataComponent {
/**
* The ID of the ePerson.
*/
@Input() ePersonId: string;
/**
* The properties of the ePerson to display.
*/
@Input() properties: string[];
/**
* Creates an instance of the EPersonDataComponent.
* @param ePersonDataService The service for retrieving ePerson data.
*/
constructor(private ePersonDataService: EPersonDataService) { }
/**
* Retrieves the EPerson data based on the provided ePersonId.
* @returns An Observable that emits the EPerson data.
*/
getEPersonData$(): Observable<EPerson> {
if (this.ePersonId) {
return this.ePersonDataService.findById(this.ePersonId, true).pipe(
getFirstCompletedRemoteData(),
getRemoteDataPayload()
);
}
}
}

View File

@@ -39,12 +39,17 @@
<tr> <tr>
<th scope="col" class="trust-col">{{'quality-assurance.event.table.trust' | translate}}</th> <th scope="col" class="trust-col">{{'quality-assurance.event.table.trust' | translate}}</th>
<th scope="col" class="title-col">{{'quality-assurance.event.table.publication' | translate}}</th> <th scope="col" class="title-col">{{'quality-assurance.event.table.publication' | translate}}</th>
<th *ngIf="hasDetailColumn() && showTopic.indexOf('/PROJECT') == -1" scope="col" class="content-col">
{{'quality-assurance.event.table.details' | translate}}
</th>
<th *ngIf="hasDetailColumn() && showTopic.indexOf('/PROJECT') !== -1" scope="col" class="content-col"> <th *ngIf="hasDetailColumn() && showTopic.indexOf('/PROJECT') !== -1" scope="col" class="content-col">
{{'quality-assurance.event.table.project-details' | translate}} {{'quality-assurance.event.table.project-details' | translate}}
</th> </th>
<ng-container *ngIf="hasDetailColumn() && (showTopic.indexOf('/REINSTATE') !== -1 || showTopic.indexOf('/WITHDRAWN') !== -1)">
<th scope="col" class="content-col">
{{'quality-assurance.event.table.reasons' | translate}}
</th>
<th scope="col" class="content-col">
{{'quality-assurance.event.table.person-who-requested' | translate}}
</th>
</ng-container>
<th scope="col" class="button-col">{{'quality-assurance.event.table.actions' | translate}}</th> <th scope="col" class="button-col">{{'quality-assurance.event.table.actions' | translate}}</th>
</tr> </tr>
</thead> </thead>
@@ -80,7 +85,8 @@
</p> </p>
</td> </td>
<td *ngIf="showTopic.indexOf('/SUBJECT') !== -1"> <td *ngIf="showTopic.indexOf('/SUBJECT') !== -1">
<p><span class="small">{{'quality-assurance.event.table.subjectValue' | translate}}</span><br><span class="badge badge-info">{{eventElement.event.message.value}}</span></p> <p><span class="small">{{'quality-assurance.event.table.subjectValue' | translate}}
</span><br><span class="badge badge-info">{{eventElement.event.message.value}}</span></p>
</td> </td>
<td *ngIf="showTopic.indexOf('/ABSTRACT') !== -1"> <td *ngIf="showTopic.indexOf('/ABSTRACT') !== -1">
<p class="abstract-container" [class.show]="showMore"> <p class="abstract-container" [class.show]="showMore">
@@ -93,6 +99,23 @@
{{ (showMore ? 'quality-assurance.event.table.less': 'quality-assurance.event.table.more') | translate }} {{ (showMore ? 'quality-assurance.event.table.less': 'quality-assurance.event.table.more') | translate }}
</button> </button>
</td> </td>
<ng-container *ngIf="showTopic.indexOf('/REINSTATE') !== -1 || showTopic.indexOf('/WITHDRAWN') !== -1">
<td>
<p>
<span *ngIf="eventElement.event.message">
<span>{{eventElement.event.message.reason}}</span><br>
</span>
</p>
</td>
<td>
<p>
<span *ngIf="eventElement.event.originalId">
<ds-eperson-data [ePersonId]="eventElement.event.originalId" [properties]="['email']"></ds-eperson-data>
</span>
</p>
</td>
</ng-container>
<td *ngIf="showTopic.indexOf('/PROJECT') !== -1"> <td *ngIf="showTopic.indexOf('/PROJECT') !== -1">
<p> <p>
{{'quality-assurance.event.table.suggestedProject' | translate}} {{'quality-assurance.event.table.suggestedProject' | translate}}
@@ -133,7 +156,7 @@
</div> </div>
</td> </td>
<td> <td>
<div class="btn-group button-width"> <div *ngIf="(isAdmin$ | async)" class="btn-group button-width">
<button *ngIf="showTopic.indexOf('/PROJECT') !== -1" <button *ngIf="showTopic.indexOf('/PROJECT') !== -1"
class="btn btn-outline-success btn-sm button-width" class="btn btn-outline-success btn-sm button-width"
ngbTooltip="{{'quality-assurance.event.action.import' | translate}}" ngbTooltip="{{'quality-assurance.event.action.import' | translate}}"
@@ -173,6 +196,16 @@
<i class="fas fa-trash-alt"></i> <i class="fas fa-trash-alt"></i>
</button> </button>
</div> </div>
<div *ngIf="!(isAdmin$ | async)" class="btn-group button-width">
<button class="btn btn-outline-danger btn-sm button-width"
ngbTooltip="{{'quality-assurance.event.action.undo' | translate}}"
container="body"
[disabled]="eventElement.isRunning"
[attr.aria-label]="'quality-assurance.event.action.undo' | translate"
(click)="openModal('UNDO', eventElement, undoModal)">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -235,3 +268,20 @@
</button> </button>
</div> </div>
</ng-template> </ng-template>
<ng-template #undoModal let-modal>
<div class="modal-header">
<h4 class="modal-title" id="undoModal">{{'quality-assurance.event.sure' | translate}}</h4>
</div>
<div class="modal-body">
<p>{{'quality-assurance.event.undo.description' | translate}}</p>
<button class="btn btn-outline-danger float-right" (click)="modal.close('do')">
<i class="fas fa-trash-alt"></i>
<span class="d-none d-sm-inline"> {{'quality-assurance.event.action.undo' | translate}}</span>
</button>
<button class="btn btn-outline-secondary" (click)="modal.close('cancel')">
<i class="fas fa-close"></i>
<span class="d-none d-sm-inline"> {{'quality-assurance.event.action.cancel' | translate}}</span>
</button>
</div>
</ng-template>

View File

@@ -43,6 +43,7 @@ import { PaginationService } from '../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { FindListOptions } from '../../../core/data/find-list-options.model'; import { FindListOptions } from '../../../core/data/find-list-options.model';
import { ItemDataService } from 'src/app/core/data/item-data.service'; import { ItemDataService } from 'src/app/core/data/item-data.service';
import { AuthorizationDataService } from 'src/app/core/data/feature-authorization/authorization-data.service';
describe('QualityAssuranceEventsComponent test suite', () => { describe('QualityAssuranceEventsComponent test suite', () => {
let fixture: ComponentFixture<QualityAssuranceEventsComponent>; let fixture: ComponentFixture<QualityAssuranceEventsComponent>;
@@ -120,6 +121,7 @@ describe('QualityAssuranceEventsComponent test suite', () => {
{ provide: TranslateService, useValue: getMockTranslateService() }, { provide: TranslateService, useValue: getMockTranslateService() },
{ provide: PaginationService, useValue: paginationService }, { provide: PaginationService, useValue: paginationService },
{ provide: ItemDataService, useValue: {} }, { provide: ItemDataService, useValue: {} },
{ provide: AuthorizationDataService, useValue: {} },
QualityAssuranceEventsComponent QualityAssuranceEventsComponent
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]

View File

@@ -26,13 +26,14 @@ import {
ProjectEntryImportModalComponent, ProjectEntryImportModalComponent,
QualityAssuranceEventData QualityAssuranceEventData
} from '../project-entry-import-modal/project-entry-import-modal.component'; } from '../project-entry-import-modal/project-entry-import-modal.component';
import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationService } from '../../../core/pagination/pagination.service';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { FindListOptions } from '../../../core/data/find-list-options.model'; import { FindListOptions } from '../../../core/data/find-list-options.model';
import { getItemPageRoute } from '../../../item-page/item-page-routing-paths'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { ItemDataService } from '../../../core/data/item-data.service'; import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import {environment} from '../../../../environments/environment'; import { NoContent } from '../../../core/shared/NoContent.model';
import { environment } from '../../../../environments/environment';
/** /**
* Component to display the Quality Assurance event list. * Component to display the Quality Assurance event list.
@@ -78,6 +79,11 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy {
* @type {string} * @type {string}
*/ */
public topic: string; public topic: string;
/**
* The sourceId of the Quality Assurance events.
* @type {string}
*/
sourceId: string;
/** /**
* The rejected/ignore reason. * The rejected/ignore reason.
* @type {string} * @type {string}
@@ -88,6 +94,7 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy {
* @type {Observable<boolean>} * @type {Observable<boolean>}
*/ */
public isEventPageLoading: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); public isEventPageLoading: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
/** /**
* The modal reference. * The modal reference.
* @type {any} * @type {any}
@@ -113,24 +120,9 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy {
protected subs: Subscription[] = []; protected subs: Subscription[] = [];
/** /**
* The target item id, retrieved from the topic-id composition. * Observable that emits a boolean value indicating whether the user is an admin.
*/ */
public targetId: string; isAdmin$: Observable<boolean>;
/**
* The URL of the item page/target.
*/
public itemPageUrl: string;
/**
* Plain topic name (without the source id)
*/
public selectedTopicName: string;
/**
* The source id, retrieved from the topic-id composition.
*/
public sourceId: string;
/** /**
* Initialize the component variables. * Initialize the component variables.
@@ -148,7 +140,7 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy {
private qualityAssuranceEventRestService: QualityAssuranceEventDataService, private qualityAssuranceEventRestService: QualityAssuranceEventDataService,
private paginationService: PaginationService, private paginationService: PaginationService,
private translateService: TranslateService, private translateService: TranslateService,
private itemService: ItemDataService, private authorizationService: AuthorizationDataService,
) { ) {
} }
@@ -157,27 +149,31 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy {
*/ */
ngOnInit(): void { ngOnInit(): void {
this.isEventPageLoading.next(true); this.isEventPageLoading.next(true);
this.isAdmin$ = this.authorizationService.isAuthorized(FeatureID.AdministratorOf);
this.activatedRoute.paramMap.pipe( this.activatedRoute.paramMap.pipe(
tap((params) => { tap((params) => {
this.sourceUrlForProjectSearch = environment.qualityAssuranceConfig.sourceUrlMapForProjectSearch[params.get('sourceId')]; this.sourceUrlForProjectSearch = environment.qualityAssuranceConfig.sourceUrlMapForProjectSearch[params.get('sourceId')];
}), this.sourceId = params.get('sourceId');
map((params) => params.get('topicId')), }),
map((params) => params.get('topicId')),
take(1), take(1),
switchMap((id: string) => { switchMap((id: string) => {
const regEx = /!/g; const regEx = /!/g;
this.showTopic = id.replace(regEx, '/'); this.showTopic = id.replace(regEx, '/');
this.topic = id; this.topic = id;
const splitList = this.showTopic?.split(':');
this.targetId = splitList.length > 2 ? splitList.pop() : null;
this.sourceId = splitList[0];
this.selectedTopicName = splitList[1];
return this.getQualityAssuranceEvents(); return this.getQualityAssuranceEvents();
}) })
).subscribe((events: QualityAssuranceEventData[]) => { ).subscribe(
this.eventsUpdated$.next(events); {
this.isEventPageLoading.next(false); next: (events: QualityAssuranceEventData[]) => {
}); this.eventsUpdated$.next(events);
this.isEventPageLoading.next(false);
},
error: (error) => {
this.isEventPageLoading.next(false);
}
}
);
} }
/** /**
@@ -187,6 +183,8 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy {
return (this.showTopic.indexOf('/PROJECT') !== -1 || return (this.showTopic.indexOf('/PROJECT') !== -1 ||
this.showTopic.indexOf('/PID') !== -1 || this.showTopic.indexOf('/PID') !== -1 ||
this.showTopic.indexOf('/SUBJECT') !== -1 || this.showTopic.indexOf('/SUBJECT') !== -1 ||
this.showTopic.indexOf('/WITHDRAWN') !== -1 ||
this.showTopic.indexOf('/REINSTATE') !== -1 ||
this.showTopic.indexOf('/ABSTRACT') !== -1 this.showTopic.indexOf('/ABSTRACT') !== -1
); );
} }
@@ -271,8 +269,14 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy {
*/ */
public executeAction(action: string, eventData: QualityAssuranceEventData): void { public executeAction(action: string, eventData: QualityAssuranceEventData): void {
eventData.isRunning = true; eventData.isRunning = true;
let operation;
if (action === 'UNDO') {
operation = this.delete(eventData);
} else {
operation = this.qualityAssuranceEventRestService.patchEvent(action, eventData.event, eventData.reason);
}
this.subs.push( this.subs.push(
this.qualityAssuranceEventRestService.patchEvent(action, eventData.event, eventData.reason).pipe( operation.pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
switchMap((rd: RemoteData<QualityAssuranceEventObject>) => { switchMap((rd: RemoteData<QualityAssuranceEventObject>) => {
if (rd.hasSucceeded) { if (rd.hasSucceeded) {
@@ -389,7 +393,7 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy {
switchMap((rd: RemoteData<PaginatedList<QualityAssuranceEventObject>>) => { switchMap((rd: RemoteData<PaginatedList<QualityAssuranceEventObject>>) => {
if (rd.hasSucceeded) { if (rd.hasSucceeded) {
this.totalElements$.next(rd.payload.totalElements); this.totalElements$.next(rd.payload.totalElements);
if (rd.payload.totalElements > 0) { if (rd.payload?.page?.length > 0) {
return this.fetchEvents(rd.payload.page); return this.fetchEvents(rd.payload.page);
} else { } else {
return of([]); return of([]);
@@ -460,29 +464,11 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy {
} }
/** /**
* Returns the page route for the given item. * Deletes a quality assurance event.
* @param item The item to get the page route for. * @param qaEvent The quality assurance event to delete.
* @returns The page route for the given item. * @returns An Observable of RemoteData containing NoContent.
*/
public getItemPageRoute(item: Item): string {
return getItemPageRoute(item);
}
/**
* Returns an Observable that emits the title of the target item.
* The target item is retrieved by its ID using the itemService.
* The title is extracted from the first metadata value of the item.
* The item page URL is also set in the component.
* @returns An Observable that emits the title of the target item.
*/ */
public getTargetItemTitle(): Observable<string> { delete(qaEvent: QualityAssuranceEventData): Observable<RemoteData<NoContent>> {
return this.itemService.findById(this.targetId).pipe( return this.qualityAssuranceEventRestService.deleteQAEvent(qaEvent);
take(1),
getFirstCompletedRemoteData(),
getRemoteDataPayload(),
tap((item: Item) => this.itemPageUrl = getItemPageRoute(item)),
map((item: Item) => item.firstMetadataValue('dc.title'))
);
} }
} }

View File

@@ -34,12 +34,12 @@
<tbody> <tbody>
<tr *ngFor="let sourceElement of (sources$ | async); let i = index"> <tr *ngFor="let sourceElement of (sources$ | async); let i = index">
<td>{{sourceElement.id}}</td> <td>{{sourceElement.id}}</td>
<td>{{sourceElement.lastEvent}}</td> <td>{{sourceElement.lastEvent | date: 'dd/MM/yyyy hh:mm' }}</td>
<td> <td>
<div class="btn-group edit-field"> <div class="btn-group edit-field">
<button <button
class="btn btn-outline-primary btn-sm" class="btn btn-outline-primary btn-sm"
title="{{'quality-assurance.button.detail' | translate }}" title="{{'quality-assurance.source-list.button.detail' | translate : { param: sourceElement.id } }}"
[routerLink]="[sourceElement.id]"> [routerLink]="[sourceElement.id]">
<span class="badge badge-info">{{sourceElement.totalEvents}}</span> <span class="badge badge-info">{{sourceElement.totalEvents}}</span>
<i class="fas fa-info fa-fw"></i> <i class="fas fa-info fa-fw"></i>

View File

@@ -38,12 +38,12 @@
<tbody> <tbody>
<tr *ngFor="let topicElement of (topics$ | async); let i = index"> <tr *ngFor="let topicElement of (topics$ | async); let i = index">
<td>{{topicElement.name}}</td> <td>{{topicElement.name}}</td>
<td>{{topicElement.lastEvent}}</td> <td>{{topicElement.lastEvent | date: 'dd/MM/yyyy hh:mm' }}</td>
<td> <td>
<div class="btn-group edit-field"> <div class="btn-group edit-field">
<button <button
class="btn btn-outline-primary btn-sm" class="btn btn-outline-primary btn-sm"
title="{{'quality-assurance.button.detail' | translate }}" title="{{'quality-assurance.topics-list.button.detail' | translate : { param: topicElement.name } }}"
[routerLink]="[getQualityAssuranceRoute(), sourceId, topicElement.id]"> [routerLink]="[getQualityAssuranceRoute(), sourceId, topicElement.id]">
<span class="badge badge-info">{{topicElement.totalEvents}}</span> <span class="badge badge-info">{{topicElement.totalEvents}}</span>
<i class="fas fa-info fa-fw"></i> <i class="fas fa-info fa-fw"></i>

View File

@@ -16,7 +16,7 @@ import { NotificationsStateService } from '../../notifications-state.service';
import { cold } from 'jasmine-marbles'; import { cold } from 'jasmine-marbles';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationService } from '../../../core/pagination/pagination.service';
import { ItemDataService } from '../../../core/data/item-data.service'; import { ItemDataService } from 'src/app/core/data/item-data.service';
describe('QualityAssuranceTopicsComponent test suite', () => { describe('QualityAssuranceTopicsComponent test suite', () => {
let fixture: ComponentFixture<QualityAssuranceTopicsComponent>; let fixture: ComponentFixture<QualityAssuranceTopicsComponent>;

View File

@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { AfterViewInit, Component, OnDestroy, OnInit } from '@angular/core';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { distinctUntilChanged, map, take, tap } from 'rxjs/operators'; import { distinctUntilChanged, map, take, tap } from 'rxjs/operators';
@@ -10,14 +10,16 @@ import {
import { hasValue } from '../../../shared/empty.util'; import { hasValue } from '../../../shared/empty.util';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { NotificationsStateService } from '../../notifications-state.service'; import { NotificationsStateService } from '../../notifications-state.service';
import {
AdminQualityAssuranceTopicsPageParams
} from '../../../admin/admin-notifications/admin-quality-assurance-topics-page/admin-quality-assurance-topics-page-resolver.service';
import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationService } from '../../../core/pagination/pagination.service';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { ItemDataService } from '../../../core/data/item-data.service'; import { ItemDataService } from '../../../core/data/item-data.service';
import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../../../core/shared/operators'; import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../../../core/shared/operators';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { getItemPageRoute } from '../../../item-page/item-page-routing-paths'; import { getItemPageRoute } from '../../../item-page/item-page-routing-paths';
import { getNotificatioQualityAssuranceRoute } from '../../../admin/admin-routing-paths'; import { getNotificatioQualityAssuranceRoute } from '../../../admin/admin-routing-paths';
import { QualityAssuranceTopicsPageParams } from '../../../quality-assurance-notifications-pages/quality-assurance-topics-page/quality-assurance-topics-page-resolver.service';
/** /**
* Component to display the Quality Assurance topic list. * Component to display the Quality Assurance topic list.
@@ -27,7 +29,7 @@ import { QualityAssuranceTopicsPageParams } from '../../../quality-assurance-not
templateUrl: './quality-assurance-topics.component.html', templateUrl: './quality-assurance-topics.component.html',
styleUrls: ['./quality-assurance-topics.component.scss'], styleUrls: ['./quality-assurance-topics.component.scss'],
}) })
export class QualityAssuranceTopicsComponent implements OnInit { export class QualityAssuranceTopicsComponent implements OnInit, OnDestroy, AfterViewInit {
/** /**
* The pagination system configuration for HTML listing. * The pagination system configuration for HTML listing.
* @type {PaginationComponentOptions} * @type {PaginationComponentOptions}
@@ -78,12 +80,14 @@ export class QualityAssuranceTopicsComponent implements OnInit {
* @param {PaginationService} paginationService * @param {PaginationService} paginationService
* @param {ActivatedRoute} activatedRoute * @param {ActivatedRoute} activatedRoute
* @param {NotificationsStateService} notificationsStateService * @param {NotificationsStateService} notificationsStateService
* @param {QualityAssuranceTopicsService} qualityAssuranceTopicsService
*/ */
constructor( constructor(
private paginationService: PaginationService, private paginationService: PaginationService,
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
private itemService: ItemDataService, private itemService: ItemDataService,
private notificationsStateService: NotificationsStateService, private notificationsStateService: NotificationsStateService,
private router: Router,
) { ) {
this.sourceId = this.activatedRoute.snapshot.params.sourceId; this.sourceId = this.activatedRoute.snapshot.params.sourceId;
this.targetId = this.activatedRoute.snapshot.params.targetId; this.targetId = this.activatedRoute.snapshot.params.targetId;
@@ -93,7 +97,15 @@ export class QualityAssuranceTopicsComponent implements OnInit {
* Component initialization. * Component initialization.
*/ */
ngOnInit(): void { ngOnInit(): void {
this.topics$ = this.notificationsStateService.getQualityAssuranceTopics(); this.topics$ = this.notificationsStateService.getQualityAssuranceTopics().pipe(
tap((topics: QualityAssuranceTopicObject[]) => {
const forward = this.activatedRoute.snapshot.queryParams?.forward === 'true';
if (topics.length === 1 && forward) {
// If there is only one topic, navigate to the first topic automatically
this.router.navigate([this.getQualityAssuranceRoute(), this.sourceId, topics[0].id]);
}
})
);
this.totalElements$ = this.notificationsStateService.getQualityAssuranceTopicsTotals(); this.totalElements$ = this.notificationsStateService.getQualityAssuranceTopicsTotals();
} }
@@ -134,7 +146,7 @@ export class QualityAssuranceTopicsComponent implements OnInit {
* Dispatch the Quality Assurance topics retrival. * Dispatch the Quality Assurance topics retrival.
*/ */
public getQualityAssuranceTopics(source: string, target?: string): void { public getQualityAssuranceTopics(source: string, target?: string): void {
this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig).pipe( this.subs.push(this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig).pipe(
distinctUntilChanged(), distinctUntilChanged(),
).subscribe((options: PaginationComponentOptions) => { ).subscribe((options: PaginationComponentOptions) => {
this.notificationsStateService.dispatchRetrieveQualityAssuranceTopics( this.notificationsStateService.dispatchRetrieveQualityAssuranceTopics(
@@ -143,7 +155,7 @@ export class QualityAssuranceTopicsComponent implements OnInit {
source, source,
target target
); );
}); }));
} }
/** /**
@@ -151,7 +163,7 @@ export class QualityAssuranceTopicsComponent implements OnInit {
* *
* @param eventsRouteParams * @param eventsRouteParams
*/ */
protected updatePaginationFromRouteParams(eventsRouteParams: QualityAssuranceTopicsPageParams) { protected updatePaginationFromRouteParams(eventsRouteParams: AdminQualityAssuranceTopicsPageParams) {
if (eventsRouteParams.currentPage) { if (eventsRouteParams.currentPage) {
this.paginationConfig.currentPage = eventsRouteParams.currentPage; this.paginationConfig.currentPage = eventsRouteParams.currentPage;
} }

View File

@@ -29,7 +29,7 @@ describe('qualityAssuranceTopicsReducer test suite', () => {
const expectedState = qualityAssuranceTopicInitialState; const expectedState = qualityAssuranceTopicInitialState;
expectedState.processing = true; expectedState.processing = true;
const action = new RetrieveAllTopicsAction(elementPerPage, currentPage, 'source', 'target'); const action = new RetrieveAllTopicsAction(elementPerPage, currentPage, 'ENRICH!MORE!ABSTRACT');
const newState = qualityAssuranceTopicsReducer(qualityAssuranceTopicInitialState, action); const newState = qualityAssuranceTopicsReducer(qualityAssuranceTopicInitialState, action);
expect(newState).toEqual(expectedState); expect(newState).toEqual(expectedState);

View File

@@ -48,7 +48,7 @@ describe('QualityAssuranceTopicsService', () => {
serviceAsAny = service; serviceAsAny = service;
}); });
describe('getTopicsBySource', () => { describe('getTopics', () => {
it('should proxy the call to qualityAssuranceTopicRestService.searchTopicsBySource', () => { it('should proxy the call to qualityAssuranceTopicRestService.searchTopicsBySource', () => {
const sortOptions = new SortOptions('name', SortDirection.ASC); const sortOptions = new SortOptions('name', SortDirection.ASC);
const findListOptions: FindListOptions = { const findListOptions: FindListOptions = {
@@ -57,7 +57,7 @@ describe('QualityAssuranceTopicsService', () => {
sort: sortOptions, sort: sortOptions,
searchParams: [new RequestParam('source', 'openaire')] searchParams: [new RequestParam('source', 'openaire')]
}; };
const result = service.getTopics(elementsPerPage, currentPage, 'openaire'); service.getTopics(elementsPerPage, currentPage, 'openaire');
expect((service as any).qualityAssuranceTopicRestService.searchTopicsBySource).toHaveBeenCalledWith(findListOptions); expect((service as any).qualityAssuranceTopicRestService.searchTopicsBySource).toHaveBeenCalledWith(findListOptions);
}); });
@@ -68,20 +68,5 @@ describe('QualityAssuranceTopicsService', () => {
const result = service.getTopics(elementsPerPage, currentPage, 'openaire'); const result = service.getTopics(elementsPerPage, currentPage, 'openaire');
expect(result).toBeObservable(expected); expect(result).toBeObservable(expected);
}); });
it('should include targetId in searchParams if set', () => {
const sortOptions = new SortOptions('name', SortDirection.ASC);
const findListOptions: FindListOptions = {
elementsPerPage: elementsPerPage,
currentPage: currentPage,
sort: sortOptions,
searchParams: [
new RequestParam('source', 'openaire'),
new RequestParam('target', '0000-0000-0000-0000-0000')
]
};
const result = service.getTopics(elementsPerPage, currentPage,'openaire', '0000-0000-0000-0000-0000');
expect((service as any).qualityAssuranceTopicRestService.searchTopicsByTarget).toHaveBeenCalledWith(findListOptions);
});
}); });
}); });

View File

@@ -10,9 +10,10 @@ import { hasValue, isNotEmpty } from '../shared/empty.util';
import { ResearcherProfile } from '../core/profile/model/researcher-profile.model'; import { ResearcherProfile } from '../core/profile/model/researcher-profile.model';
import { import {
getAllSucceededRemoteDataPayload, getAllSucceededRemoteDataPayload,
getFinishedRemoteData, getFirstCompletedRemoteData, getFinishedRemoteData,
getFirstCompletedRemoteData,
getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload,
getFirstSucceededRemoteListPayload getFirstSucceededRemoteListPayload,
} from '../core/shared/operators'; } from '../core/shared/operators';
import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';

View File

@@ -0,0 +1,55 @@
<div *ngIf="!(this.submitted$ | async); else waiting">
<div *ngIf="this.canWithdraw; else reinstateHeader" class="modal-header">
{{ 'item.qa.withdrawn.modal.header' | translate }}
<button type="button" class="close" (click)="onModalClose()" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="reason">{{ this.canWithdraw ? ('qa-withdrawn.create.modal.form.summary.label' | translate)
: ('qa-reinstate.create.modal.form.summary.label' | translate) }}</label>
<label for="reason">{{ this.canWithdraw ? ('qa-withdrawn.create.modal.form.summary2.label' | translate)
: ('qa-reinstate.create.modal.form.summary2.label' | translate) }}</label>
<textarea class="form-control" id="reason"
rows="6"
[(ngModel)]="reason"
placeholder="{{ this.canWithdraw ? ('qa-withdrown.modal.form.summary.placeholder' | translate)
: ('qa-reinstate.modal.form.summary.placeholder' | translate) }}"
name="message"></textarea>
</div>
</div>
<div class="modal-footer space-children-mr">
<button class="btn btn-outline-secondary btn-sm ml-0"
type="button"
(click)="onModalClose()"
title="{{'item.qa.withdrawn-reinstate.create.modal.button.cancel.tooltip' | translate}}">
<i class="fas fa-times fa-fw"></i> {{'item.qa.withdrawn-reinstate.create.modal.button.cancel' | translate}}
</button>
<button class="btn btn-success btn-sm ml-0"
type="submit"
(click)="onModalSubmit()"
title="{{'item.qa.withdrawn-reinstate.modal.button.confirm.tooltip' | translate}}">
<i class="fas fa-check fa-fw"></i> {{ this.canWithdraw ? ('qa-withdrown.create.modal.button.confirm' | translate)
: ('qa-reinstate.create.modal.button.confirm' | translate) }}
</button>
</div>
</div>
<ng-template #waiting>
<div class="modal-header">{{'item.qa.withdrawn.modal.submitted.header' | translate}}</div>
<div class="modal-body">
<div class="d-flex justify-content-center">
<ds-loading [showMessage]="false"></ds-loading>
</div>
</div>
</ng-template>
<ng-template #reinstateHeader>
<div *ngIf="!this.canWithdraw" class="modal-header">
{{'item.qa.reinstate.modal.header' | translate}}
<button type="button" class="close" (click)="onModalClose()" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
</ng-template>

View File

@@ -0,0 +1,74 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { ModalBeforeDismiss } from '../interfaces/modal-before-dismiss.interface';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { BehaviorSubject } from 'rxjs';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
@Component({
selector: 'ds-item-withdrawn-reinstate-modal',
templateUrl: './item-withdrawn-reinstate-modal.component.html',
styleUrls: ['./item-withdrawn-reinstate-modal.component.scss']
})
/**
* Represents a modal component for withdrawing or reinstating an item.
* Implements the ModalBeforeDismiss interface.
*/
export class ItemWithdrawnReinstateModalComponent implements ModalBeforeDismiss {
/**
* The reason for withdrawing or reinstating a suggestion.
*/
reason: string;
/**
* Indicates whether the item can be withdrawn.
*/
canWithdraw: boolean;
/**
* BehaviorSubject that represents the submitted state.
* Emits a boolean value indicating whether the form has been submitted or not.
*/
submitted$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
/**
* Event emitter for creating a QA event.
* @event createQAEvent
*/
@Output() createQAEvent: EventEmitter<string> = new EventEmitter<string>();
constructor(
protected activeModal: NgbActiveModal,
protected authorizationService: AuthorizationDataService,
) {}
/**
* Closes the modal.
*/
onModalClose() {
this.activeModal.close();
}
/**
* Determines whether the modal can be dismissed.
* @returns {boolean} True if the modal can be dismissed, false otherwise.
*/
beforeDismiss(): boolean {
// prevent the modal from being dismissed after version creation is initiated
return !this.submitted$.getValue();
}
/**
* Handles the submission of the modal form.
* Emits the reason for withdrawal or reinstatement through the createQAEvent output.
*/
onModalSubmit() {
this.submitted$.next(true);
this.createQAEvent.emit(this.reason);
}
/**
* Sets the withdrawal state of the component.
* @param state The new withdrawal state.
*/
public setWithdraw(state: boolean) {
this.canWithdraw = state;
}
}

View File

@@ -23,6 +23,10 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { Community } from '../../core/shared/community.model'; import { Community } from '../../core/shared/community.model';
import { Collection } from '../../core/shared/collection.model'; import { Collection } from '../../core/shared/collection.model';
import flatten from 'lodash/flatten'; import flatten from 'lodash/flatten';
import { DsoWithdrawnReinstateModalService } from './dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service';
import { AuthService } from 'src/app/core/auth/auth.service';
import { AuthServiceMock } from '../mocks/auth.service.mock';
import { CorrectionTypeDataService } from 'src/app/core/submission/correctiontype-data.service';
describe('DSOEditMenuResolver', () => { describe('DSOEditMenuResolver', () => {
@@ -39,6 +43,8 @@ describe('DSOEditMenuResolver', () => {
let researcherProfileService; let researcherProfileService;
let notificationsService; let notificationsService;
let translate; let translate;
let dsoWithdrawnReinstateModalService;
let correctionsDataService;
const dsoRoute = (dso: DSpaceObject) => { const dsoRoute = (dso: DSpaceObject) => {
return { return {
@@ -141,6 +147,14 @@ describe('DSOEditMenuResolver', () => {
error: {}, error: {},
}); });
dsoWithdrawnReinstateModalService = jasmine.createSpyObj('dsoWithdrawnReinstateModalService', {
openCreateWithdrawnReinstateModal: {},
});
correctionsDataService = jasmine.createSpyObj('correctionsDataService', {
findByItem: observableOf([])
});
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule], imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule],
declarations: [AdminSidebarComponent], declarations: [AdminSidebarComponent],
@@ -152,6 +166,9 @@ describe('DSOEditMenuResolver', () => {
{provide: ResearcherProfileDataService, useValue: researcherProfileService}, {provide: ResearcherProfileDataService, useValue: researcherProfileService},
{provide: TranslateService, useValue: translate}, {provide: TranslateService, useValue: translate},
{provide: NotificationsService, useValue: notificationsService}, {provide: NotificationsService, useValue: notificationsService},
{provide: DsoWithdrawnReinstateModalService, useValue: dsoWithdrawnReinstateModalService},
{provide: AuthService, useValue: new AuthServiceMock()},
{provide: CorrectionTypeDataService, useValue: correctionsDataService},
{ {
provide: NgbModal, useValue: { provide: NgbModal, useValue: {
open: () => {/*comment*/ open: () => {/*comment*/
@@ -350,7 +367,7 @@ describe('DSOEditMenuResolver', () => {
route = dsoRoute(testItem); route = dsoRoute(testItem);
}); });
it('should return Item-specific entries', (done) => { it('should return Item-specific entries', () => {
const result = resolver.getDsoMenus(testObject, route, state); const result = resolver.getDsoMenus(testObject, route, state);
combineLatest(result).pipe(map(flatten)).subscribe((menu) => { combineLatest(result).pipe(map(flatten)).subscribe((menu) => {
const orcidEntry = menu.find(entry => entry.id === 'orcid-dso'); const orcidEntry = menu.find(entry => entry.id === 'orcid-dso');
@@ -371,20 +388,18 @@ describe('DSOEditMenuResolver', () => {
expect(claimEntry.active).toBeFalse(); expect(claimEntry.active).toBeFalse();
expect(claimEntry.visible).toBeFalse(); expect(claimEntry.visible).toBeFalse();
expect(claimEntry.model.type).toEqual(MenuItemType.ONCLICK); expect(claimEntry.model.type).toEqual(MenuItemType.ONCLICK);
done();
}); });
}); });
it('should not return Community/Collection-specific entries', (done) => { it('should not return Community/Collection-specific entries', () => {
const result = resolver.getDsoMenus(testObject, route, state); const result = resolver.getDsoMenus(testObject, route, state);
combineLatest(result).pipe(map(flatten)).subscribe((menu) => { combineLatest(result).pipe(map(flatten)).subscribe((menu) => {
const subscribeEntry = menu.find(entry => entry.id === 'subscribe'); const subscribeEntry = menu.find(entry => entry.id === 'subscribe');
expect(subscribeEntry).toBeFalsy(); expect(subscribeEntry).toBeFalsy();
done();
}); });
}); });
it('should return as third part the common list ', (done) => { it('should return as third part the common list ', () => {
const result = resolver.getDsoMenus(testObject, route, state); const result = resolver.getDsoMenus(testObject, route, state);
combineLatest(result).pipe(map(flatten)).subscribe((menu) => { combineLatest(result).pipe(map(flatten)).subscribe((menu) => {
const editEntry = menu.find(entry => entry.id === 'edit-dso'); const editEntry = menu.find(entry => entry.id === 'edit-dso');
@@ -395,7 +410,6 @@ describe('DSOEditMenuResolver', () => {
expect((editEntry.model as LinkMenuItemModel).link).toEqual( expect((editEntry.model as LinkMenuItemModel).link).toEqual(
'/items/test-item-uuid/edit/metadata' '/items/test-item-uuid/edit/metadata'
); );
done();
}); });
}); });
}); });

View File

@@ -8,7 +8,9 @@ import { LinkMenuItemModel } from '../menu/menu-item/models/link.model';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { OnClickMenuItemModel } from '../menu/menu-item/models/onclick.model'; import { OnClickMenuItemModel } from '../menu/menu-item/models/onclick.model';
import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import {
getFirstCompletedRemoteData, getRemoteDataPayload,
} from '../../core/shared/operators';
import { map, switchMap } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { URLCombiner } from '../../core/url-combiner/url-combiner'; import { URLCombiner } from '../../core/url-combiner/url-combiner';
@@ -21,6 +23,11 @@ import { getDSORoute } from '../../app-routing-paths';
import { ResearcherProfileDataService } from '../../core/profile/researcher-profile-data.service'; import { ResearcherProfileDataService } from '../../core/profile/researcher-profile-data.service';
import { NotificationsService } from '../notifications/notifications.service'; import { NotificationsService } from '../notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DsoWithdrawnReinstateModalService, REQUEST_REINSTATE, REQUEST_WITHDRAWN } from './dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service';
import { AuthService } from '../../core/auth/auth.service';
import { FindListOptions } from '../../core/data/find-list-options.model';
import { RequestParam } from '../../core/cache/models/request-param.model';
import { CorrectionTypeDataService } from '../../core/submission/correctiontype-data.service';
import { SubscriptionModalComponent } from '../subscriptions/subscription-modal/subscription-modal.component'; import { SubscriptionModalComponent } from '../subscriptions/subscription-modal/subscription-modal.component';
import { Community } from '../../core/shared/community.model'; import { Community } from '../../core/shared/community.model';
import { Collection } from '../../core/shared/collection.model'; import { Collection } from '../../core/shared/collection.model';
@@ -42,6 +49,9 @@ export class DSOEditMenuResolver implements Resolve<{ [key: string]: MenuSection
protected researcherProfileService: ResearcherProfileDataService, protected researcherProfileService: ResearcherProfileDataService,
protected notificationsService: NotificationsService, protected notificationsService: NotificationsService,
protected translate: TranslateService, protected translate: TranslateService,
protected dsoWithdrawnReinstateModalService: DsoWithdrawnReinstateModalService,
private auth: AuthService,
private correctionTypeDataService: CorrectionTypeDataService
) { ) {
} }
@@ -123,14 +133,20 @@ export class DSOEditMenuResolver implements Resolve<{ [key: string]: MenuSection
*/ */
protected getItemMenu(dso): Observable<MenuSection[]> { protected getItemMenu(dso): Observable<MenuSection[]> {
if (dso instanceof Item) { if (dso instanceof Item) {
const findListTopicOptions: FindListOptions = {
searchParams: [new RequestParam('target', dso.uuid)]
};
return combineLatest([ return combineLatest([
this.authorizationService.isAuthorized(FeatureID.CanCreateVersion, dso.self), this.authorizationService.isAuthorized(FeatureID.CanCreateVersion, dso.self),
this.dsoVersioningModalService.isNewVersionButtonDisabled(dso), this.dsoVersioningModalService.isNewVersionButtonDisabled(dso),
this.dsoVersioningModalService.getVersioningTooltipMessage(dso, 'item.page.version.hasDraft', 'item.page.version.create'), this.dsoVersioningModalService.getVersioningTooltipMessage(dso, 'item.page.version.hasDraft', 'item.page.version.create'),
this.authorizationService.isAuthorized(FeatureID.CanSynchronizeWithORCID, dso.self), this.authorizationService.isAuthorized(FeatureID.CanSynchronizeWithORCID, dso.self),
this.authorizationService.isAuthorized(FeatureID.CanClaimItem, dso.self), this.authorizationService.isAuthorized(FeatureID.CanClaimItem, dso.self),
this.correctionTypeDataService.findByItem(dso.uuid, false).pipe(
getFirstCompletedRemoteData(),
getRemoteDataPayload())
]).pipe( ]).pipe(
map(([canCreateVersion, disableVersioning, versionTooltip, canSynchronizeWithOrcid, canClaimItem]) => { map(([canCreateVersion, disableVersioning, versionTooltip, canSynchronizeWithOrcid, canClaimItem, correction]) => {
const isPerson = this.getDsoType(dso) === 'person'; const isPerson = this.getDsoType(dso) === 'person';
return [ return [
{ {
@@ -174,6 +190,34 @@ export class DSOEditMenuResolver implements Resolve<{ [key: string]: MenuSection
icon: 'hand-paper', icon: 'hand-paper',
index: 3 index: 3
}, },
{
id: 'withdrawn-item',
active: false,
visible: dso.isArchived && correction?.page.some((c) => c.topic === REQUEST_WITHDRAWN),
model: {
type: MenuItemType.ONCLICK,
text:'item.page.withdrawn',
function: () => {
this.dsoWithdrawnReinstateModalService.openCreateWithdrawnReinstateModal(dso, 'request-withdrawn', dso.isArchived);
}
} as OnClickMenuItemModel,
icon: 'eye-slash',
index: 4
},
{
id: 'reinstate-item',
active: false,
visible: dso.isWithdrawn && correction?.page.some((c) => c.topic === REQUEST_REINSTATE),
model: {
type: MenuItemType.ONCLICK,
text:'item.page.reinstate',
function: () => {
this.dsoWithdrawnReinstateModalService.openCreateWithdrawnReinstateModal(dso, 'request-reinstate', dso.isArchived);
}
} as OnClickMenuItemModel,
icon: 'eye',
index: 5
}
]; ];
}), }),
); );
@@ -263,4 +307,5 @@ export class DSOEditMenuResolver implements Resolve<{ [key: string]: MenuSection
return menu; return menu;
}); });
} }
} }

View File

@@ -0,0 +1,102 @@
import { Injectable } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { Router } from '@angular/router';
import { ItemDataService } from '../../../core/data/item-data.service';
import { ItemWithdrawnReinstateModalComponent } from '../../correction-suggestion/withdrawn-reinstate-modal.component';
import {
QualityAssuranceEventDataService
} from '../../../core/notifications/qa/events/quality-assurance-event-data.service';
import {
QualityAssuranceEventObject
} from '../../../core/notifications/qa/models/quality-assurance-event.model';
import { RemoteData } from '../../../core/data/remote-data';
import { TranslateService } from '@ngx-translate/core';
import { NotificationsService } from '../../notifications/notifications.service';
import { take } from 'rxjs/operators';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { Item } from 'src/app/core/shared/item.model';
export const REQUEST_WITHDRAWN = 'REQUEST/WITHDRAWN';
export const REQUEST_REINSTATE = 'REQUEST/REINSTATE';
@Injectable({
providedIn: 'root'
})
/**
* Service for managing the withdrawn/reinstate modal for a DSO.
*/
export class DsoWithdrawnReinstateModalService {
constructor(
protected router: Router,
protected modalService: NgbModal,
protected itemService: ItemDataService,
private notificationsService: NotificationsService,
protected authorizationService: AuthorizationDataService,
private translateService: TranslateService,
protected qaEventDataService: QualityAssuranceEventDataService,
) {}
/**
* Open the create withdrawn modal for the provided dso
*/
openCreateWithdrawnReinstateModal(dso: Item, correctionType: 'request-reinstate' | 'request-withdrawn', state: boolean): void {
const target = dso.id;
// Open modal
const activeModal = this.modalService.open(ItemWithdrawnReinstateModalComponent);
(activeModal.componentInstance as ItemWithdrawnReinstateModalComponent).setWithdraw(state);
(activeModal.componentInstance as ItemWithdrawnReinstateModalComponent).createQAEvent
.pipe(
take(1)
).subscribe(
(reasone) => {
this.sendQARequest(target, correctionType, reasone);
activeModal.close();
}
);
}
/**
* Sends a quality assurance request.
*
* @param target - The target - the item's UUID.
* @param correctionType - The type of correction.
* @param reason - The reason for the request.
* Reloads the current page in order to update the withdrawn/reinstate button.
* and desplay a notification box.
*/
sendQARequest(target: string, correctionType: 'request-reinstate' | 'request-withdrawn', reason: string): void {
this.qaEventDataService.postData(target, correctionType, '', reason)
.pipe (
getFirstCompletedRemoteData()
)
.subscribe((res: RemoteData<QualityAssuranceEventObject>) => {
if (res.hasSucceeded) {
const message = (correctionType === 'request-withdrawn')
? 'correction-type.manage-relation.action.notification.withdrawn'
: 'correction-type.manage-relation.action.notification.reinstate';
this.notificationsService.success(this.translateService.get(message));
this.authorizationService.invalidateAuthorizationsRequestCache();
this.reloadPage(true);
} else {
this.notificationsService.error(this.translateService.get('correction-type.manage-relation.action.notification.error'));
}
});
}
/**
* Reloads the current page or navigates to a specified URL.
* @param self - A boolean indicating whether to reload the current page (true) or navigate to a specified URL (false).
* @param urlToNavigateTo - The URL to navigate to if `self` is false.
* skipLocationChange:true means dont update the url to / when navigating
*/
reloadPage(self: boolean, urlToNavigateTo?: string) {
const url = self ? this.router.url : urlToNavigateTo;
this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => {
this.router.navigate([`/${url}`]);
});
}
}

View File

@@ -1482,7 +1482,8 @@ export const qualityAssuranceEventObjectMissingPid: QualityAssuranceEventObject
funder: null, funder: null,
fundingProgram: null, fundingProgram: null,
jurisdiction: null, jurisdiction: null,
title: null title: null,
reason: 'Missing PID'
}, },
_links: { _links: {
self: { self: {
@@ -1519,7 +1520,8 @@ export const qualityAssuranceEventObjectMissingPid2: QualityAssuranceEventObject
funder: null, funder: null,
fundingProgram: null, fundingProgram: null,
jurisdiction: null, jurisdiction: null,
title: null title: null,
reason: 'Missing PID'
}, },
_links: { _links: {
self: { self: {
@@ -1556,7 +1558,8 @@ export const qualityAssuranceEventObjectMissingPid3: QualityAssuranceEventObject
funder: null, funder: null,
fundingProgram: null, fundingProgram: null,
jurisdiction: null, jurisdiction: null,
title: null title: null,
reason: 'Missing PID'
}, },
_links: { _links: {
self: { self: {
@@ -1593,7 +1596,8 @@ export const qualityAssuranceEventObjectMissingPid4: QualityAssuranceEventObject
funder: null, funder: null,
fundingProgram: null, fundingProgram: null,
jurisdiction: null, jurisdiction: null,
title: null title: null,
reason: 'Missing DOI'
}, },
_links: { _links: {
self: { self: {
@@ -1630,7 +1634,8 @@ export const qualityAssuranceEventObjectMissingPid5: QualityAssuranceEventObject
funder: null, funder: null,
fundingProgram: null, fundingProgram: null,
jurisdiction: null, jurisdiction: null,
title: null title: null,
reason: 'Missing PID'
}, },
_links: { _links: {
self: { self: {
@@ -1667,7 +1672,8 @@ export const qualityAssuranceEventObjectMissingPid6: QualityAssuranceEventObject
funder: null, funder: null,
fundingProgram: null, fundingProgram: null,
jurisdiction: null, jurisdiction: null,
title: null title: null,
reason: 'Missing PID'
}, },
_links: { _links: {
self: { self: {
@@ -1704,7 +1710,8 @@ export const qualityAssuranceEventObjectMissingAbstract: QualityAssuranceEventOb
funder: null, funder: null,
fundingProgram: null, fundingProgram: null,
jurisdiction: null, jurisdiction: null,
title: null title: null,
reason: 'Missing abstract'
}, },
_links: { _links: {
self: { self: {
@@ -1741,6 +1748,7 @@ export const qualityAssuranceEventObjectMissingProjectFound: QualityAssuranceEve
funder: 'EC', funder: 'EC',
fundingProgram: 'H2020', fundingProgram: 'H2020',
jurisdiction: 'EU', jurisdiction: 'EU',
reason: 'Project found',
title: 'Tracking Papyrus and Parchment Paths: An Archaeological Atlas of Coptic Literature.\nLiterary Texts in their Geographical Context: Production, Copying, Usage, Dissemination and Storage' title: 'Tracking Papyrus and Parchment Paths: An Archaeological Atlas of Coptic Literature.\nLiterary Texts in their Geographical Context: Production, Copying, Usage, Dissemination and Storage'
}, },
_links: { _links: {
@@ -1778,7 +1786,8 @@ export const qualityAssuranceEventObjectMissingProjectNotFound: QualityAssurance
funder: 'EC', funder: 'EC',
fundingProgram: 'H2021', fundingProgram: 'H2021',
jurisdiction: 'EU', jurisdiction: 'EU',
title: 'Tracking Unknown Papyrus and Parchment Paths: An Archaeological Atlas of Coptic Literature.\nLiterary Texts in their Geographical Context: Production, Copying, Usage, Dissemination and Storage' title: 'Tracking Unknown Papyrus and Parchment Paths: An Archaeological Atlas of Coptic Literature.\nLiterary Texts in their Geographical Context: Production, Copying, Usage, Dissemination and Storage',
reason: 'Project not found'
}, },
_links: { _links: {
self: { self: {
@@ -1838,8 +1847,10 @@ export function getMockNotificationsStateService(): any {
*/ */
export function getMockQualityAssuranceTopicRestService(): QualityAssuranceTopicDataService { export function getMockQualityAssuranceTopicRestService(): QualityAssuranceTopicDataService {
return jasmine.createSpyObj('QualityAssuranceTopicDataService', { return jasmine.createSpyObj('QualityAssuranceTopicDataService', {
searchTopicsBySource: jasmine.createSpy('searchTopicsBySource'), getTopic: jasmine.createSpy('getTopic'),
searchTopicsByTarget: jasmine.createSpy('searchTopicsByTarget'), searchTopicsByTarget: jasmine.createSpy('searchTopicsByTarget'),
searchTopicsBySource: jasmine.createSpy('searchTopicsBySource'),
clearFindAllTopicsRequests: jasmine.createSpy('clearFindAllTopicsRequests'),
}); });
} }

View File

@@ -282,7 +282,8 @@ import { NgxPaginationModule } from 'ngx-pagination';
import { SplitPipe } from './utils/split.pipe'; import { SplitPipe } from './utils/split.pipe';
import { ThemedUserMenuComponent } from './auth-nav-menu/user-menu/themed-user-menu.component'; import { ThemedUserMenuComponent } from './auth-nav-menu/user-menu/themed-user-menu.component';
import { ThemedLangSwitchComponent } from './lang-switch/themed-lang-switch.component'; import { ThemedLangSwitchComponent } from './lang-switch/themed-lang-switch.component';
import { IpV4Validator } from './utils/ipV4.validator'; import { QualityAssuranceEventDataService } from '../core/notifications/qa/events/quality-assurance-event-data.service';
import { QualityAssuranceSourceDataService } from '../core/notifications/qa/source/quality-assurance-source-data.service';
import { DynamicComponentLoaderDirective } from './abstract-component-loader/dynamic-component-loader.directive'; import { DynamicComponentLoaderDirective } from './abstract-component-loader/dynamic-component-loader.directive';
import { StartsWithLoaderComponent } from './starts-with/starts-with-loader.component'; import { StartsWithLoaderComponent } from './starts-with/starts-with-loader.component';
@@ -325,7 +326,7 @@ const PIPES = [
BrowserOnlyPipe, BrowserOnlyPipe,
MarkdownPipe, MarkdownPipe,
ShortNumberPipe, ShortNumberPipe,
SplitPipe, SplitPipe
]; ];
const COMPONENTS = [ const COMPONENTS = [
@@ -475,7 +476,9 @@ const ENTRY_COMPONENTS = [
const PROVIDERS = [ const PROVIDERS = [
TruncatableService, TruncatableService,
MockAdminGuard, MockAdminGuard,
AbstractTrackableComponent AbstractTrackableComponent,
QualityAssuranceEventDataService,
QualityAssuranceSourceDataService
]; ];
const DIRECTIVES = [ const DIRECTIVES = [
@@ -492,8 +495,7 @@ const DIRECTIVES = [
MetadataFieldValidator, MetadataFieldValidator,
HoverClassDirective, HoverClassDirective,
ContextHelpDirective, ContextHelpDirective,
IpV4Validator, DynamicComponentLoaderDirective,
DynamicComponentLoaderDirective
]; ];
@NgModule({ @NgModule({

View File

@@ -1,10 +1,14 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
/**
* Custom pipe to split a string into an array of substrings based on a specified separator.
* @param value - The string to be split.
* @param separator - The separator used to split the string.
* @returns An array of substrings.
*/
@Pipe({ @Pipe({
name: 'dsSplit' name: 'dsSplit'
}) })
export class SplitPipe implements PipeTransform { export class SplitPipe implements PipeTransform {
transform(value: string, separator: string): string[] { transform(value: string, separator: string): string[] {
return value.split(separator); return value.split(separator);
} }

View File

@@ -534,7 +534,7 @@
"admin.quality-assurance.page.title": "Quality Assurance", "admin.quality-assurance.page.title": "Quality Assurance",
"admin.notifications.source.breadcrumbs": "Quality Assurance Source", "admin.notifications.source.breadcrumbs": "Quality Assurance",
"admin.access-control.groups.form.tooltip.editGroupPage": "On this page, you can modify the properties and members of a group. In the top section, you can edit the group name and description, unless this is an admin group for a collection or community, in which case the group name and description are auto-generated and cannot be edited. In the following sections, you can edit group membership. See [the wiki](https://wiki.lyrasis.org/display/DSDOC7x/Create+or+manage+a+user+group) for more details.", "admin.access-control.groups.form.tooltip.editGroupPage": "On this page, you can modify the properties and members of a group. In the top section, you can edit the group name and description, unless this is an admin group for a collection or community, in which case the group name and description are auto-generated and cannot be edited. In the following sections, you can edit group membership. See [the wiki](https://wiki.lyrasis.org/display/DSDOC7x/Create+or+manage+a+user+group) for more details.",
@@ -1954,6 +1954,10 @@
"item.alerts.withdrawn": "This item has been withdrawn", "item.alerts.withdrawn": "This item has been withdrawn",
"item.alerts.reinstate-request": "Request reinstate",
"quality-assurance.event.table.person-who-requested": "Requested by",
"item.edit.authorizations.heading": "With this editor you can view and alter the policies of an item, plus alter policies of individual item components: bundles and bitstreams. Briefly, an item is a container of bundles, and bundles are containers of bitstreams. Containers usually have ADD/REMOVE/READ/WRITE policies, while bitstreams only have READ/WRITE policies.", "item.edit.authorizations.heading": "With this editor you can view and alter the policies of an item, plus alter policies of individual item components: bundles and bitstreams. Briefly, an item is a container of bundles, and bundles are containers of bitstreams. Containers usually have ADD/REMOVE/READ/WRITE policies, while bitstreams only have READ/WRITE policies.",
"item.edit.authorizations.title": "Edit item's Policies", "item.edit.authorizations.title": "Edit item's Policies",
@@ -2432,6 +2436,14 @@
"item.truncatable-part.show-less": "Collapse", "item.truncatable-part.show-less": "Collapse",
"item.qa-event-notification.check.notification-info": "There are {{num}} pending suggestions related to your account",
"item.qa-event-notification-info.check.button": "View",
"mydspace.qa-event-notification.check.notification-info": "There are {{num}} pending suggestions related to your account",
"mydspace.qa-event-notification-info.check.button": "View",
"workflow-item.search.result.delete-supervision.modal.header": "Delete Supervision Order", "workflow-item.search.result.delete-supervision.modal.header": "Delete Supervision Order",
"workflow-item.search.result.delete-supervision.modal.info": "Are you sure you want to delete Supervision Order", "workflow-item.search.result.delete-supervision.modal.info": "Are you sure you want to delete Supervision Order",
@@ -2536,6 +2548,10 @@
"item.page.version.create": "Create new version", "item.page.version.create": "Create new version",
"item.page.withdrawn": "Request a withdrawal for this item",
"item.page.reinstate": "Request reinstatement",
"item.page.version.hasDraft": "A new version cannot be created because there is an inprogress submission in the version history", "item.page.version.hasDraft": "A new version cannot be created because there is an inprogress submission in the version history",
"item.page.claim.button": "Claim", "item.page.claim.button": "Claim",
@@ -2670,6 +2686,12 @@
"item.version.create.modal.header": "New version", "item.version.create.modal.header": "New version",
"item.qa.withdrawn.modal.header": "Request withdrawal",
"item.qa.reinstate.modal.header": "Request reinstate",
"item.qa.reinstate.create.modal.header": "New version",
"item.version.create.modal.text": "Create a new version for this item", "item.version.create.modal.text": "Create a new version for this item",
"item.version.create.modal.text.startingFrom": "starting from version {{version}}", "item.version.create.modal.text.startingFrom": "starting from version {{version}}",
@@ -2678,16 +2700,44 @@
"item.version.create.modal.button.confirm.tooltip": "Create new version", "item.version.create.modal.button.confirm.tooltip": "Create new version",
"item.qa.withdrawn-reinstate.modal.button.confirm.tooltip": "Send request",
"qa-withdrown.create.modal.button.confirm": "Withdraw",
"qa-reinstate.create.modal.button.confirm": "Reinstate",
"item.version.create.modal.button.cancel": "Cancel", "item.version.create.modal.button.cancel": "Cancel",
"item.qa.withdrawn-reinstate.create.modal.button.cancel": "Cancel",
"item.version.create.modal.button.cancel.tooltip": "Do not create new version", "item.version.create.modal.button.cancel.tooltip": "Do not create new version",
"item.qa.withdrawn-reinstate.create.modal.button.cancel.tooltip": "Do not send request",
"item.version.create.modal.form.summary.label": "Summary", "item.version.create.modal.form.summary.label": "Summary",
"qa-withdrawn.create.modal.form.summary.label": "You are requesting to withdraw this item",
"qa-withdrawn.create.modal.form.summary2.label": "Please enter the reason for the withdrawal",
"qa-reinstate.create.modal.form.summary.label": "You are requesting to reinstate this item",
"qa-reinstate.create.modal.form.summary2.label": "Please enter the reason for the reinstatment",
"item.version.create.modal.form.summary.placeholder": "Insert the summary for the new version", "item.version.create.modal.form.summary.placeholder": "Insert the summary for the new version",
"qa-withdrown.modal.form.summary.placeholder": "Enter the reason for the withdrawal",
"qa-reinstate.modal.form.summary.placeholder": "Enter the reason for the reinstatement",
"item.version.create.modal.submitted.header": "Creating new version...", "item.version.create.modal.submitted.header": "Creating new version...",
"item.qa.withdrawn.modal.submitted.header": "Sending withdrawn request...",
"correction-type.manage-relation.action.notification.reinstate": "Reinstate request sent.",
"correction-type.manage-relation.action.notification.withdrawn": "Withdraw request sent.",
"item.version.create.modal.submitted.text": "The new version is being created. This may take some time if the item has a lot of relationships.", "item.version.create.modal.submitted.text": "The new version is being created. This may take some time if the item has a lot of relationships.",
"item.version.create.notification.success": "New version has been created with version number {{version}}", "item.version.create.notification.success": "New version has been created with version number {{version}}",
@@ -3216,6 +3266,8 @@
"quality-assurance.topics.description": "Below you can see all the topics received from the subscriptions to {{source}}.", "quality-assurance.topics.description": "Below you can see all the topics received from the subscriptions to {{source}}.",
"quality-assurance.topics.description-with-target": "Below you can see all the topics received from the subscriptions to {{source}} in regards to the",
"quality-assurance.source.description": "Below you can see all the notification's sources.", "quality-assurance.source.description": "Below you can see all the notification's sources.",
"quality-assurance.topics": "Current Topics", "quality-assurance.topics": "Current Topics",
@@ -3230,7 +3282,9 @@
"quality-assurance.table.actions": "Actions", "quality-assurance.table.actions": "Actions",
"quality-assurance.button.detail": "Show details", "quality-assurance.source-list.button.detail": "Show topics for {{param}}",
"quality-assurance.topics-list.button.detail": "Show suggestions for {{param}}",
"quality-assurance.noTopics": "No topics found.", "quality-assurance.noTopics": "No topics found.",
@@ -3256,12 +3310,16 @@
"quality-assurance.event.table.project-details": "Project details", "quality-assurance.event.table.project-details": "Project details",
"quality-assurance.event.table.reasons": "Reasons",
"quality-assurance.event.table.actions": "Actions", "quality-assurance.event.table.actions": "Actions",
"quality-assurance.event.action.accept": "Accept suggestion", "quality-assurance.event.action.accept": "Accept suggestion",
"quality-assurance.event.action.ignore": "Ignore suggestion", "quality-assurance.event.action.ignore": "Ignore suggestion",
"quality-assurance.event.action.undo": "DELETE",
"quality-assurance.event.action.reject": "Reject suggestion", "quality-assurance.event.action.reject": "Reject suggestion",
"quality-assurance.event.action.import": "Import project and accept suggestion", "quality-assurance.event.action.import": "Import project and accept suggestion",
@@ -3290,6 +3348,8 @@
"quality-assurance.events.back": "Back to topics", "quality-assurance.events.back": "Back to topics",
"quality-assurance.events.back-to-sources": "Back to sources",
"quality-assurance.event.table.less": "Show less", "quality-assurance.event.table.less": "Show less",
"quality-assurance.event.table.more": "Show more", "quality-assurance.event.table.more": "Show more",
@@ -3302,6 +3362,8 @@
"quality-assurance.event.ignore.description": "This operation can't be undone. Ignore this suggestion?", "quality-assurance.event.ignore.description": "This operation can't be undone. Ignore this suggestion?",
"quality-assurance.event.undo.description": "This operation can't be undone!",
"quality-assurance.event.reject.description": "This operation can't be undone. Reject this suggestion?", "quality-assurance.event.reject.description": "This operation can't be undone. Reject this suggestion?",
"quality-assurance.event.accept.description": "No DSpace project selected. A new project will be created based on the suggestion data.", "quality-assurance.event.accept.description": "No DSpace project selected. A new project will be created based on the suggestion data.",

View File

@@ -3535,21 +3535,21 @@
// "item.truncatable-part.show-less": "Collapse", // "item.truncatable-part.show-less": "Collapse",
"item.truncatable-part.show-less": "Riduci", "item.truncatable-part.show-less": "Riduci",
// "item.qa-event-notification.check.notification-info": "There are {{num}} pending review to check", // "item.qa-event-notification.check.notification-info": "There are {{num}} pending suggestions related to your account",
// TODO New key - Add a translation // TODO New key - Add a translation
"item.qa-event-notification.check.notification-info": "There are {{num}} pending review to check", "item.qa-event-notification.check.notification-info": "There are {{num}} pending suggestions related to your account",
// "item.qa-event-notification-info.check.button": "Check", // "item.qa-event-notification-info.check.button": "View",
// TODO New key - Add a translation // TODO New key - Add a translation
"item.qa-event-notification-info.check.button": "Check", "item.qa-event-notification-info.check.button": "View",
// "mydspace.qa-event-notification.check.notification-info": "There are {{num}} pending review to check", // "mydspace.qa-event-notification.check.notification-info": "There are {{num}} pending suggestions related to your account",
// TODO New key - Add a translation // TODO New key - Add a translation
"mydspace.qa-event-notification.check.notification-info": "There are {{num}} pending review to check", "mydspace.qa-event-notification.check.notification-info": "There are {{num}} pending suggestions related to your account",
// "mydspace.qa-event-notification-info.check.button": "Check", // "mydspace.qa-event-notification-info.check.button": "View",
// TODO New key - Add a translation // TODO New key - Add a translation
"mydspace.qa-event-notification-info.check.button": "Check", "mydspace.qa-event-notification-info.check.button": "View",
// "workflow-item.search.result.delete-supervision.modal.header": "Delete Supervision Order", // "workflow-item.search.result.delete-supervision.modal.header": "Delete Supervision Order",
"workflow-item.search.result.delete-supervision.modal.header": "Elimina l'ordine di supervisione", "workflow-item.search.result.delete-supervision.modal.header": "Elimina l'ordine di supervisione",
@@ -7073,25 +7073,6 @@
// "submission.workspace.generic.view-help": "Select this option to view the item's metadata.", // "submission.workspace.generic.view-help": "Select this option to view the item's metadata.",
"submission.workspace.generic.view-help": "Seleziona questa opzione per vedere i metadata dell'item.", "submission.workspace.generic.view-help": "Seleziona questa opzione per vedere i metadata dell'item.",
// "submission.section.section-coar-notify.dropdown.no-data": "No data available",
// TODO New key - a translation
"submission.section.section-coar-notify.dropdown.no-data": "No data available",
// "submission.section.section-coar-notify.dropdown.select-none": "Select none",
// TODO New key - a translation
"submission.section.section-coar-notify.dropdown.select-none": "Select none",
// "submission.section.section-coar-notify.small.notification": "Select a service for {{ pattern }} of this item",
// TODO New key - a translation
"submission.section.section-coar-notify.small.notification": "Select a service for {{ pattern }} of this item",
// "submission.section.section-coar-notify.selection.description": "Selected service's description:",
// TODO New key - a translation
"submission.section.section-coar-notify.selection.description": "Selected service's description:",
// "submission.section.section-coar-notify.notification.error": "The selected service is not suitable for the current item.Please check the description for details about which record can be managed by this service.",
// TODO New key - a translation
"submission.section.section-coar-notify.notification.error": "The selected service is not suitable for the current item.Please check the description for details about which record can be managed by this service.",
// "subscriptions.title": "Subscriptions", // "subscriptions.title": "Subscriptions",
"subscriptions.title": "Sottoscrizioni", "subscriptions.title": "Sottoscrizioni",

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -109,6 +109,8 @@
--ds-item-page-img-field-default-inline-height: 24px; --ds-item-page-img-field-default-inline-height: 24px;
--ds-qa-logo-width: 100px;
--ds-process-overview-table-nb-processes-badge-size: 0.5em; --ds-process-overview-table-nb-processes-badge-size: 0.5em;
--ds-process-overview-table-id-column-width: 120px; --ds-process-overview-table-id-column-width: 120px;
--ds-process-overview-table-name-column-width: auto; --ds-process-overview-table-name-column-width: auto;

View File

@@ -304,7 +304,7 @@ const DECLARATIONS = [
NgxGalleryModule, NgxGalleryModule,
FormModule, FormModule,
RequestCopyModule, RequestCopyModule,
NotificationsModule NotificationsModule,
], ],
declarations: DECLARATIONS, declarations: DECLARATIONS,
}) })