diff --git a/src/app/admin/admin-notifications/admin-notifications-routing-paths.ts b/src/app/admin/admin-notifications/admin-notifications-routing-paths.ts index f92a96d242..9fcabedd64 100644 --- a/src/app/admin/admin-notifications/admin-notifications-routing-paths.ts +++ b/src/app/admin/admin-notifications/admin-notifications-routing-paths.ts @@ -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 PUBLICATION_CLAIMS_PATH = 'publication-claim'; -export function getQualityAssuranceRoute(id: string) { - return new URLCombiner(getNotificationsModuleRoute(), QUALITY_ASSURANCE_EDIT_PATH, id).toString(); +export function getQualityAssuranceEditRoute() { + return `/${QUALITY_ASSURANCE_EDIT_PATH}`; } diff --git a/src/app/admin/admin-notifications/admin-notifications-routing.module.ts b/src/app/admin/admin-notifications/admin-notifications-routing.module.ts index 755f2e784b..e00a88cbe2 100644 --- a/src/app/admin/admin-notifications/admin-notifications-routing.module.ts +++ b/src/app/admin/admin-notifications/admin-notifications-routing.module.ts @@ -6,34 +6,22 @@ import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.r import { I18nBreadcrumbsService } from '../../core/breadcrumbs/i18n-breadcrumbs.service'; import { PUBLICATION_CLAIMS_PATH } from './admin-notifications-routing-paths'; 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 { 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 { 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 { SourceDataResolver -} from '../../quality-assurance-notifications-pages/quality-assurance-source-page-component/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'; - +} from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-data.resolver'; @NgModule({ imports: [ @@ -56,11 +44,11 @@ import { { canActivate: [ AuthenticatedGuard ], path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId`, - component: QualityAssuranceTopicsPageComponent, + component: AdminQualityAssuranceTopicsPageComponent, pathMatch: 'full', resolve: { breadcrumb: QualityAssuranceBreadcrumbResolver, - openaireQualityAssuranceTopicsParams: QualityAssuranceTopicsPageResolver + openaireQualityAssuranceTopicsParams: AdminQualityAssuranceTopicsPageResolver }, data: { title: 'admin.quality-assurance.page.title', @@ -70,12 +58,27 @@ import { }, { canActivate: [ AuthenticatedGuard ], - path: `${QUALITY_ASSURANCE_EDIT_PATH}`, - component: QualityAssuranceSourcePageComponent, + path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId/target/:targetId`, + component: AdminQualityAssuranceTopicsPageComponent, pathMatch: 'full', resolve: { 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 }, data: { @@ -87,11 +90,11 @@ import { { canActivate: [ AuthenticatedGuard ], path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId/:topicId`, - component: QualityAssuranceEventsPageComponent, + component: AdminQualityAssuranceEventsPageComponent, pathMatch: 'full', resolve: { breadcrumb: QualityAssuranceBreadcrumbResolver, - openaireQualityAssuranceEventsParams: QualityAssuranceEventsPageResolver + openaireQualityAssuranceEventsParams: AdminQualityAssuranceEventsPageResolver }, data: { title: 'admin.notifications.event.page.title', @@ -106,10 +109,10 @@ import { I18nBreadcrumbsService, AdminNotificationsPublicationClaimPageResolver, SourceDataResolver, - QualityAssuranceSourcePageResolver, - QualityAssuranceTopicsPageResolver, - QualityAssuranceEventsPageResolver, - QualityAssuranceSourcePageResolver, + AdminQualityAssuranceSourcePageResolver, + AdminQualityAssuranceTopicsPageResolver, + AdminQualityAssuranceEventsPageResolver, + AdminQualityAssuranceSourcePageResolver, QualityAssuranceBreadcrumbResolver, QualityAssuranceBreadcrumbService ] diff --git a/src/app/admin/admin-routing-paths.ts b/src/app/admin/admin-routing-paths.ts index 508d352c50..144b9d09bf 100644 --- a/src/app/admin/admin-routing-paths.ts +++ b/src/app/admin/admin-routing-paths.ts @@ -1,25 +1,18 @@ import { URLCombiner } from '../core/url-combiner/url-combiner'; 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 NOTIFICATIONS_MODULE_PATH = 'notifications'; -export const LDN_PATH = 'ldn'; - export function getRegistriesModuleRoute() { return new URLCombiner(getAdminModuleRoute(), REGISTRIES_MODULE_PATH).toString(); } -export function getLdnServicesModuleRoute() { - return new URLCombiner(getAdminModuleRoute(), LDN_PATH).toString(); +export function getNotificationsModuleRoute() { + return new URLCombiner(getAdminModuleRoute(), NOTIFICATIONS_MODULE_PATH).toString(); } export function getNotificatioQualityAssuranceRoute() { return new URLCombiner(`/${NOTIFICATIONS_MODULE_PATH}`, getQualityAssuranceEditRoute()).toString(); } - - -export function getNotificationsModuleRoute() { - return new URLCombiner(getAdminModuleRoute(), NOTIFICATIONS_MODULE_PATH).toString(); -} diff --git a/src/app/admin/admin-routing.module.ts b/src/app/admin/admin-routing.module.ts index c96431744e..c17dd5554f 100644 --- a/src/app/admin/admin-routing.module.ts +++ b/src/app/admin/admin-routing.module.ts @@ -6,72 +6,64 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component'; import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; 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 { NotifyInfoGuard } from '../core/coar-notify/notify-info/notify-info.guard'; +import { + SiteAdministratorGuard +} from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; @NgModule({ imports: [ RouterModule.forChild([ - { - path: NOTIFICATIONS_MODULE_PATH, - loadChildren: () => import('./admin-notifications/admin-notifications.module') - .then((m) => m.AdminNotificationsModule), - }, { path: REGISTRIES_MODULE_PATH, loadChildren: () => import('./admin-registries/admin-registries.module') .then((m) => m.AdminRegistriesModule), + canActivate: [SiteAdministratorGuard] }, { path: 'search', resolve: { breadcrumb: I18nBreadcrumbResolver }, component: AdminSearchPageComponent, - data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' } + data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' }, + canActivate: [SiteAdministratorGuard] }, { path: 'workflow', resolve: { breadcrumb: I18nBreadcrumbResolver }, component: AdminWorkflowPageComponent, - data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' } + data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' }, + canActivate: [SiteAdministratorGuard] }, { path: 'curation-tasks', resolve: { breadcrumb: I18nBreadcrumbResolver }, 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', resolve: { breadcrumb: I18nBreadcrumbResolver }, 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', resolve: { breadcrumb: I18nBreadcrumbResolver }, component: BatchImportPageComponent, - data: { title: 'admin.batch-import.title', breadcrumbKey: 'admin.batch-import' } - }, - { - 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] + data: { title: 'admin.batch-import.title', breadcrumbKey: 'admin.batch-import' }, + canActivate: [SiteAdministratorGuard] }, { path: 'system-wide-alert', resolve: { breadcrumb: I18nBreadcrumbResolver }, 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: [ I18nBreadcrumbResolver, diff --git a/src/app/app-routing-paths.ts b/src/app/app-routing-paths.ts index 3722383745..63b11ec80e 100644 --- a/src/app/app-routing-paths.ts +++ b/src/app/app-routing-paths.ts @@ -133,3 +133,10 @@ export const SUBSCRIPTIONS_MODULE_PATH = 'subscriptions'; export function getSubscriptionsModuleRoute() { return `/${SUBSCRIPTIONS_MODULE_PATH}`; } + +export const EDIT_ITEM_PATH = 'edit-items'; +export function getEditItemPageRoute() { + return `/${EDIT_ITEM_PATH}`; +} +export const CORRECTION_TYPE_PATH = 'corrections'; + diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index c23457f3d3..4766e36ff8 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -3,9 +3,6 @@ import { RouterModule, NoPreloading } from '@angular/router'; import { AuthBlockingGuard } from './core/auth/auth-blocking.guard'; import { AuthenticatedGuard } from './core/auth/authenticated.guard'; -import { - SiteAdministratorGuard -} from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; import { ACCESS_CONTROL_MODULE_PATH, ADMIN_MODULE_PATH, @@ -157,7 +154,13 @@ import { ForgotPasswordCheckGuard } from './core/rest-property/forgot-password-c path: ADMIN_MODULE_PATH, loadChildren: () => import('./admin/admin.module') .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, @@ -251,7 +254,7 @@ import { ForgotPasswordCheckGuard } from './core/rest-property/forgot-password-c .then((m) => m.SubscriptionsPageRoutingModule), canActivate: [AuthenticatedGuard] }, - { path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent }, + { path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent } ] } ], { diff --git a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.service.ts b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.service.ts index 187f40abda..53aba9fa0d 100644 --- a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.service.ts +++ b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.service.ts @@ -2,7 +2,10 @@ import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; import { BreadcrumbsProviderService } from './breadcrumbsProviderService'; import { Observable, of as observableOf } from 'rxjs'; import { Injectable } from '@angular/core'; +import { map } from 'rxjs/operators'; +import { getFirstCompletedRemoteData } from '../shared/operators'; 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'; constructor( + protected qualityAssuranceService: QualityAssuranceTopicDataService, private translationService: TranslateService, ) { @@ -28,14 +32,18 @@ export class QualityAssuranceBreadcrumbService implements BreadcrumbsProviderSer * @param url The url to use as a link for this breadcrumb */ getBreadcrumbs(key: string, url: string): Observable { - const args = key.split(':'); - const sourceId = args[0]; - const topicId = args.length > 2 ? args[args.length - 1] : args[1]; + const sourceId = key.split(':')[0]; + const topicId = key.split(':')[2]; 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(topicId, undefined)]); + new Breadcrumb(topicId.replace(/[!:]/g, '/'), undefined)]; + }) + ); } else { return observableOf([new Breadcrumb(this.translationService.instant(this.QUALITY_ASSURANCE_BREADCRUMB_KEY), url), new Breadcrumb(sourceId, `${url}${sourceId}`)]); diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index dd8419634f..119b993faf 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -185,19 +185,9 @@ import { FlatBrowseDefinition } from './shared/flat-browse-definition.model'; import { ValueListBrowseDefinition } from './shared/value-list-browse-definition.model'; import { NonHierarchicalBrowseDefinition } from './shared/non-hierarchical-browse-definition'; 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 { LdnService } from '../admin/admin-ldn-services/ldn-services-model/ldn-services.model'; -import { LdnItemfiltersService } from '../admin/admin-ldn-services/ldn-services-data/ldn-itemfilters-data.service'; -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'; - +import { CorrectionTypeDataService } from './submission/correctiontype-data.service'; +import { SuggestionTarget } from './suggestion-notifications/models/suggestion-target.model'; +import { SuggestionSource } from './suggestion-notifications/models/suggestion-source.model'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -321,10 +311,7 @@ const PROVIDERS = [ OrcidQueueDataService, OrcidHistoryDataService, SupervisionOrderDataService, - LdnServicesService, - LdnItemfiltersService, - CoarNotifyConfigDataService, - NotifyRequestsStatusDataService + CorrectionTypeDataService ]; /** @@ -405,11 +392,7 @@ export const models = ItemRequest, BulkAccessConditionOptions, SuggestionTarget, - SuggestionSource, - LdnService, - Itemfilter, - SubmissionCoarNotifyConfig, - NotifyRequestsStatus, + SuggestionSource ]; @NgModule({ diff --git a/src/app/core/notifications/qa/events/quality-assurance-event-data.service.ts b/src/app/core/notifications/qa/events/quality-assurance-event-data.service.ts index 6c333cc6f5..e266ace080 100644 --- a/src/app/core/notifications/qa/events/quality-assurance-event-data.service.ts +++ b/src/app/core/notifications/qa/events/quality-assurance-event-data.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { find, take } from 'rxjs/operators'; +import { find, switchMap, take } from 'rxjs/operators'; import { ReplaceOperation } from 'fast-json-patch'; 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 { hasValue } from '../../../../shared/empty.util'; 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. @@ -210,4 +215,38 @@ export class QualityAssuranceEventDataService extends IdentifiableDataService(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> { + 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(requestId); + }) + ); + } + + public deleteQAEvent(qaEvent: QualityAssuranceEventData): Observable> { + return this.deleteData.delete(qaEvent.id); + } + } diff --git a/src/app/core/notifications/qa/models/quality-assurance-event.model.ts b/src/app/core/notifications/qa/models/quality-assurance-event.model.ts index 0cdb4a5745..1d66e5bb28 100644 --- a/src/app/core/notifications/qa/models/quality-assurance-event.model.ts +++ b/src/app/core/notifications/qa/models/quality-assurance-event.model.ts @@ -28,6 +28,8 @@ export interface SourceQualityAssuranceEventMessageObject { */ type: string; + reason: string; + /** * The value suggested by Notifications */ diff --git a/src/app/core/notifications/qa/source/quality-assurance-source-data.service.ts b/src/app/core/notifications/qa/source/quality-assurance-source-data.service.ts index 130e7261d1..f6a58fdd45 100644 --- a/src/app/core/notifications/qa/source/quality-assurance-source-data.service.ts +++ b/src/app/core/notifications/qa/source/quality-assurance-source-data.service.ts @@ -16,7 +16,7 @@ import { PaginatedList } from '../../../data/paginated-list.model'; import { FindListOptions } from '../../../data/find-list-options.model'; import { IdentifiableDataService } from '../../../data/base/identifiable-data.service'; 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. diff --git a/src/app/core/notifications/qa/topics/quality-assurance-topic-data.service.spec.ts b/src/app/core/notifications/qa/topics/quality-assurance-topic-data.service.spec.ts index 183d0a42f2..bade6cace5 100644 --- a/src/app/core/notifications/qa/topics/quality-assurance-topic-data.service.spec.ts +++ b/src/app/core/notifications/qa/topics/quality-assurance-topic-data.service.spec.ts @@ -81,8 +81,9 @@ describe('QualityAssuranceTopicDataService', () => { 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).searchData, 'searchBy').and.callThrough(); }); 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> for the object with the given URL', () => { - const result = service.searchTopicsBySource(); - const expected = cold('(a)', { - a: paginatedListRD - }); - expect(result).toBeObservable(expected); - }); - }); - describe('getTopic', () => { it('should call findByHref', (done) => { service.getTopic(qualityAssuranceTopicObjectMorePid.id).subscribe( @@ -153,4 +129,5 @@ describe('QualityAssuranceTopicDataService', () => { expect(result).toBeObservable(expected); }); }); + }); diff --git a/src/app/core/notifications/qa/topics/quality-assurance-topic-data.service.ts b/src/app/core/notifications/qa/topics/quality-assurance-topic-data.service.ts index 86c8f5eeaa..919aaac71a 100644 --- a/src/app/core/notifications/qa/topics/quality-assurance-topic-data.service.ts +++ b/src/app/core/notifications/qa/topics/quality-assurance-topic-data.service.ts @@ -15,7 +15,7 @@ import { FindListOptions } from '../../../data/find-list-options.model'; import { IdentifiableDataService } from '../../../data/base/identifiable-data.service'; import { dataService } from '../../../data/base/data-service.decorator'; 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'; /** diff --git a/src/app/core/submission/correctiontype-data.service.ts b/src/app/core/submission/correctiontype-data.service.ts new file mode 100644 index 0000000000..8a5bbb1fb8 --- /dev/null +++ b/src/app/core/submission/correctiontype-data.service.ts @@ -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 { + protected linkPath = 'correctiontypes'; + protected searchByTopic = 'findByTopic'; + protected searchFindByItem = 'findByItem'; + private searchData: SearchDataImpl; + + 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>} the correction type + */ + getCorrectionTypeById(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true): Observable> { + 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>> { + 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 { + 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]; + }) + ); + } +} diff --git a/src/app/core/submission/models/correctiontype.model.ts b/src/app/core/submission/models/correctiontype.model.ts new file mode 100644 index 0000000000..9329fa88d8 --- /dev/null +++ b/src/app/core/submission/models/correctiontype.model.ts @@ -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; + }; +} diff --git a/src/app/item-page/alerts/item-alerts.component.html b/src/app/item-page/alerts/item-alerts.component.html index cd71d23a91..f6304340f3 100644 --- a/src/app/item-page/alerts/item-alerts.component.html +++ b/src/app/item-page/alerts/item-alerts.component.html @@ -6,7 +6,10 @@ diff --git a/src/app/item-page/alerts/item-alerts.component.spec.ts b/src/app/item-page/alerts/item-alerts.component.spec.ts index a933eb6a58..47a4852cf1 100644 --- a/src/app/item-page/alerts/item-alerts.component.spec.ts +++ b/src/app/item-page/alerts/item-alerts.component.spec.ts @@ -4,16 +4,41 @@ import { TranslateModule } from '@ngx-translate/core'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { Item } from '../../core/shared/item.model'; 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', () => { let component: ItemAlertsComponent; let fixture: ComponentFixture; 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(() => { + authorizationService = jasmine.createSpyObj('authorizationService', ['isAuthorized']); + dsoWithdrawnReinstateModalService = jasmine.createSpyObj('dsoWithdrawnReinstateModalService', ['openCreateWithdrawnReinstateModal']); + correctionTypeDataService = jasmine.createSpyObj('correctionTypeDataService', ['findByItem']); TestBed.configureTestingModule({ declarations: [ItemAlertsComponent], imports: [TranslateModule.forRoot()], + providers: [ + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: DsoWithdrawnReinstateModalService, useValue: dsoWithdrawnReinstateModalService }, + { provide: CorrectionTypeDataService, useValue: correctionTypeDataService } + ], schemas: [NO_ERRORS_SCHEMA] }) .compileComponents(); @@ -21,7 +46,9 @@ describe('ItemAlertsComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(ItemAlertsComponent); + component = fixture.componentInstance; + component.item = itemMock; fixture.detectChanges(); }); @@ -61,6 +88,7 @@ describe('ItemAlertsComponent', () => { isWithdrawn: true }); component.item = item; + (correctionTypeDataService.findByItem).and.returnValue(createSuccessfulRemoteDataObject$([])); fixture.detectChanges(); }); @@ -76,6 +104,7 @@ describe('ItemAlertsComponent', () => { isWithdrawn: false }); component.item = item; + (correctionTypeDataService.findByItem).and.returnValue(createSuccessfulRemoteDataObject$([])); fixture.detectChanges(); }); @@ -84,4 +113,43 @@ describe('ItemAlertsComponent', () => { 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); + }); + }); + + }); }); diff --git a/src/app/item-page/alerts/item-alerts.component.ts b/src/app/item-page/alerts/item-alerts.component.ts index 2b1df58c9f..025dafb425 100644 --- a/src/app/item-page/alerts/item-alerts.component.ts +++ b/src/app/item-page/alerts/item-alerts.component.ts @@ -1,6 +1,12 @@ import { Component, Input } from '@angular/core'; import { Item } from '../../core/shared/item.model'; 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({ selector: 'ds-item-alerts', @@ -21,4 +27,37 @@ export class ItemAlertsComponent { * @type {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 { + 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); + } } diff --git a/src/app/item-page/item-shared.module.ts b/src/app/item-page/item-shared.module.ts index 8b7243acde..49f575cbb3 100644 --- a/src/app/item-page/item-shared.module.ts +++ b/src/app/item-page/item-shared.module.ts @@ -30,11 +30,15 @@ import { RelatedItemsComponent } from './simple/related-items/related-items-comp import { ThemedMetadataRepresentationListComponent } 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'; const ENTRY_COMPONENTS = [ ItemVersionsDeleteModalComponent, ItemVersionsSummaryModalComponent, + ItemWithdrawnReinstateModalComponent ]; diff --git a/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.html b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.html index f64b654728..77370f462d 100644 --- a/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.html +++ b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.html @@ -1,13 +1,21 @@ - - + +
- +
+ +
-
{{'item.qa-event-notification.check.notification-info' | translate : {num: - source.totalEvents } }}
+
+ {{'item.qa-event-notification.check.notification-info' | translate : {num: source.totalEvents } }} +
+ [queryParams]="{ forward: true }" + class="btn btn-primary align-self-center"> + {{'item.qa-event-notification-info.check.button' | translate}} +
diff --git a/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.scss b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.scss index ab33b46fca..2a62342b7c 100644 --- a/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.scss +++ b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.scss @@ -1,8 +1,13 @@ - .source-logo { max-height: var(--ds-header-logo-height); } +.source-logo-container { + width: var(--ds-qa-logo-width); + display: flex; + justify-content: center; +} + .sections-gap { gap: 1rem; } diff --git a/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.spec.ts b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.spec.ts index 32ceece46c..ce231affee 100644 --- a/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.spec.ts +++ b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.spec.ts @@ -1,34 +1,40 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - 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 { 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 { 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 { NotificationsService } from '../../../shared/notifications/notifications.service'; import { ObjectCacheService } from '../../../core/cache/object-cache.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 { HALEndpointService } from '../../../core/shared/hal-endpoint.service'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; 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', () => { let component: QaEventNotificationComponent; let fixture: ComponentFixture; - let qualityAssuranceSourceDataServiceStub: any; - const obj = createSuccessfulRemoteDataObject$(createPaginatedList([new QualityAssuranceSourceObject()])); - const item = Object.assign({ uuid: '1234' }); + const obj = Object.assign(new QualityAssuranceSourceObject(), { + id: 'sourceName:target', + source: 'sourceName', + target: 'target', + totalEvents: 1 + }); + const objPL = createSuccessfulRemoteDataObject$(createPaginatedList([obj])); + const item = Object.assign({ uuid: '1234' }); beforeEach(async () => { + qualityAssuranceSourceDataServiceStub = { - getSourcesByTarget: () => obj + getSourcesByTarget: () => objPL }; await TestBed.configureTestingModule({ imports: [CommonModule, TranslateModule.forRoot()], @@ -37,22 +43,31 @@ describe('QaEventNotificationComponent', () => { { provide: QualityAssuranceSourceDataService, useValue: qualityAssuranceSourceDataServiceStub }, { provide: RequestService, useValue: {} }, { provide: NotificationsService, useValue: {} }, - { provide: HALEndpointService, useValue: new HALEndpointServiceStub('test')}, + { provide: HALEndpointService, useValue: new HALEndpointServiceStub('test') }, ObjectCacheService, RemoteDataBuildService, provideMockStore({}) ], - schemas: [NO_ERRORS_SCHEMA] }) .compileComponents(); - fixture = TestBed.createComponent(QaEventNotificationComponent); component = fixture.componentInstance; component.item = item; + component.sources$ = of([obj]); fixture.detectChanges(); }); it('should create', () => { 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'); + }); }); diff --git a/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.ts b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.ts index 09e9a18ed0..1557a65a0e 100644 --- a/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.ts +++ b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.ts @@ -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 { getFirstCompletedRemoteData, getPaginatedListPayload, getRemoteDataPayload } from '../../../core/shared/operators'; -import { Observable, filter } from 'rxjs'; -import { AlertType } from '../../../shared/alert/alert-type'; +import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; +import { Observable } from 'rxjs'; import { FindListOptions } from '../../../core/data/find-list-options.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 { QualityAssuranceSourceObject } from '../../../core/notifications/qa/models/quality-assurance-source.model'; -import { PaginatedList } from 'src/app/core/data/paginated-list.model'; -import { hasValue } from 'src/app/shared/empty.util'; +import { catchError, map } from 'rxjs/operators'; +import { RemoteData } from '../../../core/data/remote-data'; +import { getNotificatioQualityAssuranceRoute } from '../../../admin/admin-routing-paths'; +import { PaginatedList } from '../../../core/data/paginated-list.model'; @Component({ 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. */ -export class QaEventNotificationComponent { - +export class QaEventNotificationComponent implements OnChanges { /** * The item to display quality assurance event notifications for. */ @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; constructor( 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. @@ -46,12 +53,16 @@ export class QaEventNotificationComponent { const findListTopicOptions: FindListOptions = { searchParams: [new RequestParam('target', this.item.uuid)] }; - return this.qualityAssuranceSourceDataService.getSourcesByTarget(findListTopicOptions) + return this.qualityAssuranceSourceDataService.getSourcesByTarget(findListTopicOptions, false) .pipe( getFirstCompletedRemoteData(), - getRemoteDataPayload(), - filter((pl: PaginatedList) => hasValue(pl)), - getPaginatedListPayload(), + map((data: RemoteData>) => { + if (data.hasSucceeded) { + return data.payload.page; + } + return []; + }), + catchError(() => []) ); } diff --git a/src/app/my-dspace-page/my-dspace-page.component.html b/src/app/my-dspace-page/my-dspace-page.component.html index 70bcf1b7bc..cfae8e07a8 100644 --- a/src/app/my-dspace-page/my-dspace-page.component.html +++ b/src/app/my-dspace-page/my-dspace-page.component.html @@ -1,7 +1,7 @@
+ -
+
+
- {{ - "mydspace.qa-event-notification.check.notification-info" - | translate : { num: source.totalEvents } - }} + {{ "mydspace.qa-event-notification.check.notification-info" | translate : { num: source.totalEvents } }}
+ + +

+ + {{eventElement.event.message.reason}}
+
+

+ + +

+ + + +

+ +
+

{{'quality-assurance.event.table.suggestedProject' | translate}} @@ -133,7 +156,7 @@

-
+
+
+ +
@@ -235,3 +268,20 @@
+ + + + + diff --git a/src/app/notifications/qa/events/quality-assurance-events.component.spec.ts b/src/app/notifications/qa/events/quality-assurance-events.component.spec.ts index 2d6d45377b..c69a9108f9 100644 --- a/src/app/notifications/qa/events/quality-assurance-events.component.spec.ts +++ b/src/app/notifications/qa/events/quality-assurance-events.component.spec.ts @@ -43,6 +43,7 @@ import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; import { FindListOptions } from '../../../core/data/find-list-options.model'; 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', () => { let fixture: ComponentFixture; @@ -120,6 +121,7 @@ describe('QualityAssuranceEventsComponent test suite', () => { { provide: TranslateService, useValue: getMockTranslateService() }, { provide: PaginationService, useValue: paginationService }, { provide: ItemDataService, useValue: {} }, + { provide: AuthorizationDataService, useValue: {} }, QualityAssuranceEventsComponent ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/notifications/qa/events/quality-assurance-events.component.ts b/src/app/notifications/qa/events/quality-assurance-events.component.ts index 6ff3bccb2f..60550a8baf 100644 --- a/src/app/notifications/qa/events/quality-assurance-events.component.ts +++ b/src/app/notifications/qa/events/quality-assurance-events.component.ts @@ -26,13 +26,14 @@ import { ProjectEntryImportModalComponent, QualityAssuranceEventData } 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 { Item } from '../../../core/shared/item.model'; import { FindListOptions } from '../../../core/data/find-list-options.model'; -import { getItemPageRoute } from '../../../item-page/item-page-routing-paths'; -import { ItemDataService } from '../../../core/data/item-data.service'; -import {environment} from '../../../../environments/environment'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; +import { NoContent } from '../../../core/shared/NoContent.model'; +import { environment } from '../../../../environments/environment'; /** * Component to display the Quality Assurance event list. @@ -78,6 +79,11 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy { * @type {string} */ public topic: string; + /** + * The sourceId of the Quality Assurance events. + * @type {string} + */ + sourceId: string; /** * The rejected/ignore reason. * @type {string} @@ -88,6 +94,7 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy { * @type {Observable} */ public isEventPageLoading: BehaviorSubject = new BehaviorSubject(false); + /** * The modal reference. * @type {any} @@ -113,24 +120,9 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy { 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; - - /** - * 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; + isAdmin$: Observable; /** * Initialize the component variables. @@ -148,7 +140,7 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy { private qualityAssuranceEventRestService: QualityAssuranceEventDataService, private paginationService: PaginationService, private translateService: TranslateService, - private itemService: ItemDataService, + private authorizationService: AuthorizationDataService, ) { } @@ -157,27 +149,31 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy { */ ngOnInit(): void { this.isEventPageLoading.next(true); - + this.isAdmin$ = this.authorizationService.isAuthorized(FeatureID.AdministratorOf); this.activatedRoute.paramMap.pipe( - tap((params) => { - this.sourceUrlForProjectSearch = environment.qualityAssuranceConfig.sourceUrlMapForProjectSearch[params.get('sourceId')]; - }), - map((params) => params.get('topicId')), + tap((params) => { + this.sourceUrlForProjectSearch = environment.qualityAssuranceConfig.sourceUrlMapForProjectSearch[params.get('sourceId')]; + this.sourceId = params.get('sourceId'); + }), + map((params) => params.get('topicId')), take(1), switchMap((id: string) => { const regEx = /!/g; this.showTopic = id.replace(regEx, '/'); 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(); }) - ).subscribe((events: QualityAssuranceEventData[]) => { - this.eventsUpdated$.next(events); - this.isEventPageLoading.next(false); - }); + ).subscribe( + { + 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 || this.showTopic.indexOf('/PID') !== -1 || this.showTopic.indexOf('/SUBJECT') !== -1 || + this.showTopic.indexOf('/WITHDRAWN') !== -1 || + this.showTopic.indexOf('/REINSTATE') !== -1 || this.showTopic.indexOf('/ABSTRACT') !== -1 ); } @@ -271,8 +269,14 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy { */ public executeAction(action: string, eventData: QualityAssuranceEventData): void { 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.qualityAssuranceEventRestService.patchEvent(action, eventData.event, eventData.reason).pipe( + operation.pipe( getFirstCompletedRemoteData(), switchMap((rd: RemoteData) => { if (rd.hasSucceeded) { @@ -389,7 +393,7 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy { switchMap((rd: RemoteData>) => { if (rd.hasSucceeded) { this.totalElements$.next(rd.payload.totalElements); - if (rd.payload.totalElements > 0) { + if (rd.payload?.page?.length > 0) { return this.fetchEvents(rd.payload.page); } else { return of([]); @@ -460,29 +464,11 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy { } /** - * Returns the page route for the given item. - * @param item The item to get the page route for. - * @returns The page route for the given item. - */ - 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. + * Deletes a quality assurance event. + * @param qaEvent The quality assurance event to delete. + * @returns An Observable of RemoteData containing NoContent. */ - public getTargetItemTitle(): Observable { - return this.itemService.findById(this.targetId).pipe( - take(1), - getFirstCompletedRemoteData(), - getRemoteDataPayload(), - tap((item: Item) => this.itemPageUrl = getItemPageRoute(item)), - map((item: Item) => item.firstMetadataValue('dc.title')) - ); + delete(qaEvent: QualityAssuranceEventData): Observable> { + return this.qualityAssuranceEventRestService.deleteQAEvent(qaEvent); } - } diff --git a/src/app/notifications/qa/source/quality-assurance-source.component.html b/src/app/notifications/qa/source/quality-assurance-source.component.html index 0f6cf18402..543304aacc 100644 --- a/src/app/notifications/qa/source/quality-assurance-source.component.html +++ b/src/app/notifications/qa/source/quality-assurance-source.component.html @@ -34,12 +34,12 @@ {{sourceElement.id}} - {{sourceElement.lastEvent}} + {{sourceElement.lastEvent | date: 'dd/MM/yyyy hh:mm' }}
+
+ + + + + + + + + + + + diff --git a/src/app/shared/correction-suggestion/item-withdrawn-reinstate-modal.component.scss b/src/app/shared/correction-suggestion/item-withdrawn-reinstate-modal.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/correction-suggestion/withdrawn-reinstate-modal.component.ts b/src/app/shared/correction-suggestion/withdrawn-reinstate-modal.component.ts new file mode 100644 index 0000000000..de842eb1e5 --- /dev/null +++ b/src/app/shared/correction-suggestion/withdrawn-reinstate-modal.component.ts @@ -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 = new BehaviorSubject(false); + /** + * Event emitter for creating a QA event. + * @event createQAEvent + */ + @Output() createQAEvent: EventEmitter = new EventEmitter(); + + 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; + } +} diff --git a/src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts b/src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts index e28a416f23..14b4313ac2 100644 --- a/src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts +++ b/src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts @@ -23,6 +23,10 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { Community } from '../../core/shared/community.model'; import { Collection } from '../../core/shared/collection.model'; 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', () => { @@ -39,6 +43,8 @@ describe('DSOEditMenuResolver', () => { let researcherProfileService; let notificationsService; let translate; + let dsoWithdrawnReinstateModalService; + let correctionsDataService; const dsoRoute = (dso: DSpaceObject) => { return { @@ -141,6 +147,14 @@ describe('DSOEditMenuResolver', () => { error: {}, }); + dsoWithdrawnReinstateModalService = jasmine.createSpyObj('dsoWithdrawnReinstateModalService', { + openCreateWithdrawnReinstateModal: {}, + }); + + correctionsDataService = jasmine.createSpyObj('correctionsDataService', { + findByItem: observableOf([]) + }); + TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule], declarations: [AdminSidebarComponent], @@ -152,6 +166,9 @@ describe('DSOEditMenuResolver', () => { {provide: ResearcherProfileDataService, useValue: researcherProfileService}, {provide: TranslateService, useValue: translate}, {provide: NotificationsService, useValue: notificationsService}, + {provide: DsoWithdrawnReinstateModalService, useValue: dsoWithdrawnReinstateModalService}, + {provide: AuthService, useValue: new AuthServiceMock()}, + {provide: CorrectionTypeDataService, useValue: correctionsDataService}, { provide: NgbModal, useValue: { open: () => {/*comment*/ @@ -350,7 +367,7 @@ describe('DSOEditMenuResolver', () => { route = dsoRoute(testItem); }); - it('should return Item-specific entries', (done) => { + it('should return Item-specific entries', () => { const result = resolver.getDsoMenus(testObject, route, state); combineLatest(result).pipe(map(flatten)).subscribe((menu) => { const orcidEntry = menu.find(entry => entry.id === 'orcid-dso'); @@ -371,20 +388,18 @@ describe('DSOEditMenuResolver', () => { expect(claimEntry.active).toBeFalse(); expect(claimEntry.visible).toBeFalse(); 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); combineLatest(result).pipe(map(flatten)).subscribe((menu) => { const subscribeEntry = menu.find(entry => entry.id === 'subscribe'); 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); combineLatest(result).pipe(map(flatten)).subscribe((menu) => { const editEntry = menu.find(entry => entry.id === 'edit-dso'); @@ -395,7 +410,6 @@ describe('DSOEditMenuResolver', () => { expect((editEntry.model as LinkMenuItemModel).link).toEqual( '/items/test-item-uuid/edit/metadata' ); - done(); }); }); }); diff --git a/src/app/shared/dso-page/dso-edit-menu.resolver.ts b/src/app/shared/dso-page/dso-edit-menu.resolver.ts index 1ade457840..bcded3acd5 100644 --- a/src/app/shared/dso-page/dso-edit-menu.resolver.ts +++ b/src/app/shared/dso-page/dso-edit-menu.resolver.ts @@ -8,7 +8,9 @@ import { LinkMenuItemModel } from '../menu/menu-item/models/link.model'; import { Item } from '../../core/shared/item.model'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; 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 { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; 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 { NotificationsService } from '../notifications/notifications.service'; 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 { Community } from '../../core/shared/community.model'; import { Collection } from '../../core/shared/collection.model'; @@ -42,6 +49,9 @@ export class DSOEditMenuResolver implements Resolve<{ [key: string]: MenuSection protected researcherProfileService: ResearcherProfileDataService, protected notificationsService: NotificationsService, 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 { if (dso instanceof Item) { + const findListTopicOptions: FindListOptions = { + searchParams: [new RequestParam('target', dso.uuid)] + }; return combineLatest([ this.authorizationService.isAuthorized(FeatureID.CanCreateVersion, dso.self), this.dsoVersioningModalService.isNewVersionButtonDisabled(dso), this.dsoVersioningModalService.getVersioningTooltipMessage(dso, 'item.page.version.hasDraft', 'item.page.version.create'), this.authorizationService.isAuthorized(FeatureID.CanSynchronizeWithORCID, dso.self), this.authorizationService.isAuthorized(FeatureID.CanClaimItem, dso.self), + this.correctionTypeDataService.findByItem(dso.uuid, false).pipe( + getFirstCompletedRemoteData(), + getRemoteDataPayload()) ]).pipe( - map(([canCreateVersion, disableVersioning, versionTooltip, canSynchronizeWithOrcid, canClaimItem]) => { + map(([canCreateVersion, disableVersioning, versionTooltip, canSynchronizeWithOrcid, canClaimItem, correction]) => { const isPerson = this.getDsoType(dso) === 'person'; return [ { @@ -174,6 +190,34 @@ export class DSOEditMenuResolver implements Resolve<{ [key: string]: MenuSection icon: 'hand-paper', 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; }); } + } diff --git a/src/app/shared/dso-page/dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service.ts b/src/app/shared/dso-page/dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service.ts new file mode 100644 index 0000000000..d5f47ed57b --- /dev/null +++ b/src/app/shared/dso-page/dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service.ts @@ -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) => { + 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}`]); + }); + } +} + diff --git a/src/app/shared/mocks/notifications.mock.ts b/src/app/shared/mocks/notifications.mock.ts index 78fe62c164..86f7b74a80 100644 --- a/src/app/shared/mocks/notifications.mock.ts +++ b/src/app/shared/mocks/notifications.mock.ts @@ -1482,7 +1482,8 @@ export const qualityAssuranceEventObjectMissingPid: QualityAssuranceEventObject funder: null, fundingProgram: null, jurisdiction: null, - title: null + title: null, + reason: 'Missing PID' }, _links: { self: { @@ -1519,7 +1520,8 @@ export const qualityAssuranceEventObjectMissingPid2: QualityAssuranceEventObject funder: null, fundingProgram: null, jurisdiction: null, - title: null + title: null, + reason: 'Missing PID' }, _links: { self: { @@ -1556,7 +1558,8 @@ export const qualityAssuranceEventObjectMissingPid3: QualityAssuranceEventObject funder: null, fundingProgram: null, jurisdiction: null, - title: null + title: null, + reason: 'Missing PID' }, _links: { self: { @@ -1593,7 +1596,8 @@ export const qualityAssuranceEventObjectMissingPid4: QualityAssuranceEventObject funder: null, fundingProgram: null, jurisdiction: null, - title: null + title: null, + reason: 'Missing DOI' }, _links: { self: { @@ -1630,7 +1634,8 @@ export const qualityAssuranceEventObjectMissingPid5: QualityAssuranceEventObject funder: null, fundingProgram: null, jurisdiction: null, - title: null + title: null, + reason: 'Missing PID' }, _links: { self: { @@ -1667,7 +1672,8 @@ export const qualityAssuranceEventObjectMissingPid6: QualityAssuranceEventObject funder: null, fundingProgram: null, jurisdiction: null, - title: null + title: null, + reason: 'Missing PID' }, _links: { self: { @@ -1704,7 +1710,8 @@ export const qualityAssuranceEventObjectMissingAbstract: QualityAssuranceEventOb funder: null, fundingProgram: null, jurisdiction: null, - title: null + title: null, + reason: 'Missing abstract' }, _links: { self: { @@ -1741,6 +1748,7 @@ export const qualityAssuranceEventObjectMissingProjectFound: QualityAssuranceEve funder: 'EC', fundingProgram: 'H2020', 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' }, _links: { @@ -1778,7 +1786,8 @@ export const qualityAssuranceEventObjectMissingProjectNotFound: QualityAssurance funder: 'EC', fundingProgram: 'H2021', 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: { self: { @@ -1838,8 +1847,10 @@ export function getMockNotificationsStateService(): any { */ export function getMockQualityAssuranceTopicRestService(): QualityAssuranceTopicDataService { return jasmine.createSpyObj('QualityAssuranceTopicDataService', { - searchTopicsBySource: jasmine.createSpy('searchTopicsBySource'), + getTopic: jasmine.createSpy('getTopic'), searchTopicsByTarget: jasmine.createSpy('searchTopicsByTarget'), + searchTopicsBySource: jasmine.createSpy('searchTopicsBySource'), + clearFindAllTopicsRequests: jasmine.createSpy('clearFindAllTopicsRequests'), }); } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index ddeec2d836..e31ff2cac1 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -282,7 +282,8 @@ import { NgxPaginationModule } from 'ngx-pagination'; import { SplitPipe } from './utils/split.pipe'; import { ThemedUserMenuComponent } from './auth-nav-menu/user-menu/themed-user-menu.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 { StartsWithLoaderComponent } from './starts-with/starts-with-loader.component'; @@ -325,7 +326,7 @@ const PIPES = [ BrowserOnlyPipe, MarkdownPipe, ShortNumberPipe, - SplitPipe, + SplitPipe ]; const COMPONENTS = [ @@ -475,7 +476,9 @@ const ENTRY_COMPONENTS = [ const PROVIDERS = [ TruncatableService, MockAdminGuard, - AbstractTrackableComponent + AbstractTrackableComponent, + QualityAssuranceEventDataService, + QualityAssuranceSourceDataService ]; const DIRECTIVES = [ @@ -492,8 +495,7 @@ const DIRECTIVES = [ MetadataFieldValidator, HoverClassDirective, ContextHelpDirective, - IpV4Validator, - DynamicComponentLoaderDirective + DynamicComponentLoaderDirective, ]; @NgModule({ diff --git a/src/app/shared/utils/split.pipe.ts b/src/app/shared/utils/split.pipe.ts index 4da1b1323e..e4d0f2cc49 100644 --- a/src/app/shared/utils/split.pipe.ts +++ b/src/app/shared/utils/split.pipe.ts @@ -1,10 +1,14 @@ 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({ name: 'dsSplit' }) export class SplitPipe implements PipeTransform { - transform(value: string, separator: string): string[] { return value.split(separator); } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 3881d06647..8331a3cbeb 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -534,7 +534,7 @@ "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.", @@ -1954,6 +1954,10 @@ "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.title": "Edit item's Policies", @@ -2432,6 +2436,14 @@ "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.info": "Are you sure you want to delete Supervision Order", @@ -2536,6 +2548,10 @@ "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.claim.button": "Claim", @@ -2670,6 +2686,12 @@ "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.startingFrom": "starting from version {{version}}", @@ -2678,16 +2700,44 @@ "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.qa.withdrawn-reinstate.create.modal.button.cancel": "Cancel", + "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", + "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", + "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.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.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-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.topics": "Current Topics", @@ -3230,7 +3282,9 @@ "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.", @@ -3256,12 +3310,16 @@ "quality-assurance.event.table.project-details": "Project details", + "quality-assurance.event.table.reasons": "Reasons", + "quality-assurance.event.table.actions": "Actions", "quality-assurance.event.action.accept": "Accept 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.import": "Import project and accept suggestion", @@ -3290,6 +3348,8 @@ "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.more": "Show more", @@ -3302,6 +3362,8 @@ "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.accept.description": "No DSpace project selected. A new project will be created based on the suggestion data.", diff --git a/src/assets/i18n/it.json5 b/src/assets/i18n/it.json5 index c3bd0e225d..0960b9fae1 100644 --- a/src/assets/i18n/it.json5 +++ b/src/assets/i18n/it.json5 @@ -3535,21 +3535,21 @@ // "item.truncatable-part.show-less": "Collapse", "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 - "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 - "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 - "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 - "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": "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": "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": "Sottoscrizioni", diff --git a/src/assets/images/qa-DSpaceUsers-logo.png b/src/assets/images/qa-DSpaceUsers-logo.png new file mode 100644 index 0000000000..40b59feeaa Binary files /dev/null and b/src/assets/images/qa-DSpaceUsers-logo.png differ diff --git a/src/styles/_custom_variables.scss b/src/styles/_custom_variables.scss index 5fe93b5ae0..25f93803ec 100644 --- a/src/styles/_custom_variables.scss +++ b/src/styles/_custom_variables.scss @@ -109,6 +109,8 @@ --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-id-column-width: 120px; --ds-process-overview-table-name-column-width: auto; diff --git a/src/themes/custom/lazy-theme.module.ts b/src/themes/custom/lazy-theme.module.ts index 0f5edbbae8..14bf19ab96 100644 --- a/src/themes/custom/lazy-theme.module.ts +++ b/src/themes/custom/lazy-theme.module.ts @@ -304,7 +304,7 @@ const DECLARATIONS = [ NgxGalleryModule, FormModule, RequestCopyModule, - NotificationsModule + NotificationsModule, ], declarations: DECLARATIONS, })