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 07a98aa080..e00a88cbe2 100644 --- a/src/app/admin/admin-notifications/admin-notifications-routing.module.ts +++ b/src/app/admin/admin-notifications/admin-notifications-routing.module.ts @@ -14,6 +14,9 @@ import { AdminQualityAssuranceTopicsPageResolver } from './admin-quality-assuran 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 { @@ -55,6 +58,21 @@ import { }, { canActivate: [ AuthenticatedGuard ], + path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId/target/:targetId`, + component: AdminQualityAssuranceTopicsPageComponent, + pathMatch: 'full', + resolve: { + breadcrumb: I18nBreadcrumbResolver, + 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', diff --git a/src/app/admin/admin-routing-paths.ts b/src/app/admin/admin-routing-paths.ts index 30f801cecb..144b9d09bf 100644 --- a/src/app/admin/admin-routing-paths.ts +++ b/src/app/admin/admin-routing-paths.ts @@ -1,5 +1,6 @@ import { URLCombiner } from '../core/url-combiner/url-combiner'; import { getAdminModuleRoute } from '../app-routing-paths'; +import { getQualityAssuranceEditRoute } from './admin-notifications/admin-notifications-routing-paths'; export const REGISTRIES_MODULE_PATH = 'registries'; export const NOTIFICATIONS_MODULE_PATH = 'notifications'; @@ -11,3 +12,7 @@ export function getRegistriesModuleRoute() { export function getNotificationsModuleRoute() { return new URLCombiner(getAdminModuleRoute(), NOTIFICATIONS_MODULE_PATH).toString(); } + +export function getNotificatioQualityAssuranceRoute() { + return new URLCombiner(`/${NOTIFICATIONS_MODULE_PATH}`, getQualityAssuranceEditRoute()).toString(); +} diff --git a/src/app/admin/admin-routing.module.ts b/src/app/admin/admin-routing.module.ts index a7d19a6935..c17dd5554f 100644 --- a/src/app/admin/admin-routing.module.ts +++ b/src/app/admin/admin-routing.module.ts @@ -6,57 +6,62 @@ 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 { REGISTRIES_MODULE_PATH, NOTIFICATIONS_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 { + 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' } + 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] }, ]) ], diff --git a/src/app/app-routing-paths.ts b/src/app/app-routing-paths.ts index fe2837c6e3..2f52c64366 100644 --- a/src/app/app-routing-paths.ts +++ b/src/app/app-routing-paths.ts @@ -132,3 +132,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 a14000aef4..e94ec51215 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -3,9 +3,6 @@ import { NoPreloading, RouterModule } 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, @@ -41,6 +38,7 @@ import { ServerCheckGuard } from './core/server-check/server-check.guard'; import { SUGGESTION_MODULE_PATH } from './suggestions-page/suggestions-page-routing-paths'; import { MenuResolver } from './menu.resolver'; import { ThemedPageErrorComponent } from './page-error/themed-page-error.component'; +import { NOTIFICATIONS_MODULE_PATH } from './admin/admin-routing-paths'; import { ForgotPasswordCheckGuard } from './core/rest-property/forgot-password-check-guard.guard'; @NgModule({ @@ -159,7 +157,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: 'login', @@ -247,7 +251,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 209ae0722c..53aba9fa0d 100644 --- a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.service.ts +++ b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.service.ts @@ -33,7 +33,7 @@ export class QualityAssuranceBreadcrumbService implements BreadcrumbsProviderSer */ getBreadcrumbs(key: string, url: string): Observable { const sourceId = key.split(':')[0]; - const topicId = key.split(':')[1]; + const topicId = key.split(':')[2]; if (topicId) { return this.qualityAssuranceService.getTopic(topicId).pipe( @@ -41,7 +41,7 @@ export class QualityAssuranceBreadcrumbService implements BreadcrumbsProviderSer 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 { diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index f151f10f66..119b993faf 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -185,6 +185,7 @@ 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 { CorrectionTypeDataService } from './submission/correctiontype-data.service'; import { SuggestionTarget } from './suggestion-notifications/models/suggestion-target.model'; import { SuggestionSource } from './suggestion-notifications/models/suggestion-source.model'; @@ -309,7 +310,8 @@ const PROVIDERS = [ OrcidAuthService, OrcidQueueDataService, OrcidHistoryDataService, - SupervisionOrderDataService + SupervisionOrderDataService, + CorrectionTypeDataService ]; /** 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 7f7e68afaa..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. @@ -84,6 +89,16 @@ export class QualityAssuranceEventDataService extends IdentifiableDataService[]): Observable>> { + return this.searchData.searchBy('findByTopic', options, true, true, ...linksToFollow); + } + /** * Clear findByTopic requests from cache */ @@ -200,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 03a5da2e8c..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,6 +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 '../../../data/base/search-data'; /** * The service handling all Quality Assurance source REST requests. @@ -25,6 +26,9 @@ import { FindAllData, FindAllDataImpl } from '../../../data/base/find-all-data'; export class QualityAssuranceSourceDataService extends IdentifiableDataService { private findAllData: FindAllData; + private searchAllData: SearchData; + + private searchByTargetMethod = 'byTarget'; /** * Initialize service variables @@ -43,6 +47,7 @@ export class QualityAssuranceSourceDataService extends IdentifiableDataService[]): Observable> { return this.findById(id, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } + + /** + * Retrieves a paginated list of QualityAssuranceSourceObject objects that are associated with a given target object. + * @param options The options for the search query. + * @param useCachedVersionIfAvailable Whether to use a cached version of the data if available. + * @param reRequestOnStale Whether to re-request the data if the cached version is stale. + * @param linksToFollow The links to follow to retrieve the data. + * @returns An observable that emits a RemoteData object containing the paginated list of QualityAssuranceSourceObject objects. + */ + public getSourcesByTarget(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.searchAllData.searchBy(this.searchByTargetMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } } 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 360e6b1ccd..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 @@ -83,20 +83,27 @@ describe('QualityAssuranceTopicDataService', () => { spyOn((service as any).findAllData, 'findAll').and.callThrough(); spyOn((service as any), 'findById').and.callThrough(); + spyOn((service as any).searchData, 'searchBy').and.callThrough(); }); - describe('getTopics', () => { - it('should call findListByHref', (done) => { - service.getTopics().subscribe( - (res) => { - expect((service as any).findAllData.findAll).toHaveBeenCalledWith({}, true, true); - } + describe('searchTopicsByTarget', () => { + it('should call searchData.searchBy with the correct parameters', () => { + const options = { elementsPerPage: 10 }; + const useCachedVersionIfAvailable = true; + const reRequestOnStale = true; + + service.searchTopicsByTarget(options, useCachedVersionIfAvailable, reRequestOnStale); + + expect((service as any).searchData.searchBy).toHaveBeenCalledWith( + 'byTarget', + options, + useCachedVersionIfAvailable, + reRequestOnStale ); - done(); }); it('should return a RemoteData> for the object with the given URL', () => { - const result = service.getTopics(); + const result = service.searchTopicsByTarget(); const expected = cold('(a)', { a: paginatedListRD }); 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 2bf5195bf1..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,6 +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 '../../../data/base/search-data'; import { FindAllData, FindAllDataImpl } from '../../../data/base/find-all-data'; /** @@ -25,6 +26,10 @@ import { FindAllData, FindAllDataImpl } from '../../../data/base/find-all-data'; export class QualityAssuranceTopicDataService extends IdentifiableDataService { private findAllData: FindAllData; + private searchData: SearchData; + + private searchByTargetMethod = 'byTarget'; + private searchBySourceMethod = 'bySource'; /** * Initialize service variables @@ -43,23 +48,31 @@ export class QualityAssuranceTopicDataService extends IdentifiableDataService>> - * The list of Quality Assurance topics. + * Search for Quality Assurance topics. + * @param options The search options. + * @param useCachedVersionIfAvailable Whether to use cached version if available. + * @param reRequestOnStale Whether to re-request on stale. + * @param linksToFollow The links to follow. + * @returns An observable of remote data containing a paginated list of Quality Assurance topics. */ - public getTopics(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { - return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + public searchTopicsByTarget(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.searchData.searchBy(this.searchByTargetMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } + + /** + * Searches for quality assurance topics by source. + * @param options The search options. + * @param useCachedVersionIfAvailable Whether to use a cached version if available. + * @param reRequestOnStale Whether to re-request the data if it's stale. + * @param linksToFollow The links to follow. + * @returns An observable of the remote data containing the paginated list of quality assurance topics. + */ + public searchTopicsBySource(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.searchData.searchBy(this.searchBySourceMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } /** 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-page.module.ts b/src/app/item-page/item-page.module.ts index a8d41d1535..5aa1b6e508 100644 --- a/src/app/item-page/item-page.module.ts +++ b/src/app/item-page/item-page.module.ts @@ -60,6 +60,7 @@ import { ThemedItemAlertsComponent } from './alerts/themed-item-alerts.component import { ThemedFullFileSectionComponent } from './full/field-components/file-section/themed-full-file-section.component'; +import { QaEventNotificationComponent } from './simple/qa-event-notification/qa-event-notification.component'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -103,6 +104,7 @@ const DECLARATIONS = [ ItemAlertsComponent, ThemedItemAlertsComponent, BitstreamRequestACopyPageComponent, + QaEventNotificationComponent ]; @NgModule({ 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/item-page.component.html b/src/app/item-page/simple/item-page.component.html index cc9983bb35..37a5e0c4cb 100644 --- a/src/app/item-page/simple/item-page.component.html +++ b/src/app/item-page/simple/item-page.component.html @@ -2,6 +2,7 @@
+ 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 new file mode 100644 index 0000000000..77370f462d --- /dev/null +++ b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.html @@ -0,0 +1,22 @@ + + +
+
+ +
+
+
+ {{'item.qa-event-notification.check.notification-info' | translate : {num: source.totalEvents } }} +
+ +
+
+
+
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 new file mode 100644 index 0000000000..2a62342b7c --- /dev/null +++ b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.scss @@ -0,0 +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 new file mode 100644 index 0000000000..ce231affee --- /dev/null +++ b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.spec.ts @@ -0,0 +1,73 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { QaEventNotificationComponent } from './qa-event-notification.component'; +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 { CommonModule } from '@angular/common'; +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 { provideMockStore } from '@ngrx/store/testing'; +import { HALEndpointService } from '../../../core/shared/hal-endpoint.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 = 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: () => objPL + }; + await TestBed.configureTestingModule({ + imports: [CommonModule, TranslateModule.forRoot()], + declarations: [QaEventNotificationComponent, SplitPipe], + providers: [ + { provide: QualityAssuranceSourceDataService, useValue: qualityAssuranceSourceDataServiceStub }, + { provide: RequestService, useValue: {} }, + { provide: NotificationsService, useValue: {} }, + { provide: HALEndpointService, useValue: new HALEndpointServiceStub('test') }, + ObjectCacheService, + RemoteDataBuildService, + provideMockStore({}) + ], + }) + .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 new file mode 100644 index 0000000000..1557a65a0e --- /dev/null +++ b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.ts @@ -0,0 +1,76 @@ +import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { Item } from '../../../core/shared/item.model'; +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 { QualityAssuranceSourceDataService } from '../../../core/notifications/qa/source/quality-assurance-source-data.service'; +import { QualityAssuranceSourceObject } from '../../../core/notifications/qa/models/quality-assurance-source.model'; +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', + templateUrl: './qa-event-notification.component.html', + styleUrls: ['./qa-event-notification.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [QualityAssuranceSourceDataService] +}) +/** + * Component for displaying quality assurance event notifications for an item. + */ +export class QaEventNotificationComponent implements OnChanges { + /** + * The item to display quality assurance event notifications for. + */ + @Input() item: Item; + + /** + * An observable that emits an array of QualityAssuranceSourceObject. + */ + 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. + * Note: sourceId is composed as: id: "sourceName:" + */ + getQualityAssuranceSources$(): Observable { + const findListTopicOptions: FindListOptions = { + searchParams: [new RequestParam('target', this.item.uuid)] + }; + return this.qualityAssuranceSourceDataService.getSourcesByTarget(findListTopicOptions, false) + .pipe( + getFirstCompletedRemoteData(), + map((data: RemoteData>) => { + if (data.hasSucceeded) { + return data.payload.page; + } + return []; + }), + catchError(() => []) + ); + } + + /** + * Returns the quality assurance route. + * @returns The quality assurance route. + */ + getQualityAssuranceRoute(): string { + return getNotificatioQualityAssuranceRoute(); + } +} diff --git a/src/app/menu.resolver.ts b/src/app/menu.resolver.ts index fc6eb00195..9272ed06ea 100644 --- a/src/app/menu.resolver.ts +++ b/src/app/menu.resolver.ts @@ -171,7 +171,8 @@ export class MenuResolver implements Resolve { this.authorizationService.isAuthorized(FeatureID.AdministratorOf), this.authorizationService.isAuthorized(FeatureID.CanSubmit), this.authorizationService.isAuthorized(FeatureID.CanEditItem), - ]).subscribe(([isCollectionAdmin, isCommunityAdmin, isSiteAdmin, canSubmit, canEditItem]) => { + this.authorizationService.isAuthorized(FeatureID.CanSeeQA) + ]).subscribe(([isCollectionAdmin, isCommunityAdmin, isSiteAdmin, canSubmit, canEditItem, canSeeQa]) => { const newSubMenuList = [ { id: 'new_community', @@ -362,6 +363,40 @@ export class MenuResolver implements Resolve { icon: 'heartbeat', index: 11 }, + /* Notifications */ + { + id: 'notifications', + active: false, + visible: canSeeQa || isSiteAdmin, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.notifications' + } as TextMenuItemModel, + icon: 'bell', + index: 4 + }, + { + id: 'notifications_quality-assurance', + parentID: 'notifications', + active: false, + visible: canSeeQa, + model: { + type: MenuItemType.LINK, + text: 'menu.section.quality-assurance', + link: '/notifications/quality-assurance' + } as LinkMenuItemModel, + }, + { + id: 'notifications_publication-claim', + parentID: 'notifications', + active: false, + visible: isSiteAdmin, + model: { + type: MenuItemType.LINK, + text: 'menu.section.notifications_publication-claim', + link: '/notifications/' + PUBLICATION_CLAIMS_PATH + } as LinkMenuItemModel, + }, ]; menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, Object.assign(menuSection, { shouldPersistOnRouteChange: true @@ -531,46 +566,9 @@ export class MenuResolver implements Resolve { * Create menu sections dependent on whether or not the current user is a site administrator */ createSiteAdministratorMenuSections() { - combineLatest([ - this.authorizationService.isAuthorized(FeatureID.AdministratorOf), - this.authorizationService.isAuthorized(FeatureID.CanSeeQA) - ]) - .subscribe(([authorized, canSeeQA]) => { + this.authorizationService.isAuthorized(FeatureID.AdministratorOf) + .subscribe((authorized) => { const menuList = [ - /* Notifications */ - { - id: 'notifications', - active: false, - visible: authorized && canSeeQA, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.notifications' - } as TextMenuItemModel, - icon: 'bell', - index: 4 - }, - { - id: 'notifications_quality-assurance', - parentID: 'notifications', - active: false, - visible: authorized, - model: { - type: MenuItemType.LINK, - text: 'menu.section.quality-assurance', - link: '/admin/notifications/quality-assurance' - } as LinkMenuItemModel, - }, - { - id: 'notifications_publication-claim', - parentID: 'notifications', - active: false, - visible: authorized, - model: { - type: MenuItemType.LINK, - text: 'menu.section.notifications_publication-claim', - link: '/admin/notifications/' + PUBLICATION_CLAIMS_PATH - } as LinkMenuItemModel, - }, /* Admin Search */ { id: 'admin_search', 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 c5e49b0cec..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,6 +1,7 @@
- + +
+ +
+
+ +
+
+
+ {{ "mydspace.qa-event-notification.check.notification-info" | translate : { num: source.totalEvents } }} +
+ +
+
+
+ diff --git a/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.scss b/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.scss new file mode 100644 index 0000000000..2a62342b7c --- /dev/null +++ b/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.scss @@ -0,0 +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/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.ts b/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.ts new file mode 100644 index 0000000000..63e10bb565 --- /dev/null +++ b/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.ts @@ -0,0 +1,46 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { QualityAssuranceSourceDataService } from '../../core/notifications/qa/source/quality-assurance-source-data.service'; +import { getFirstCompletedRemoteData, getPaginatedListPayload, getRemoteDataPayload } from '../../core/shared/operators'; +import { Observable, of } from 'rxjs'; +import { QualityAssuranceSourceObject } from './../../core/notifications/qa/models/quality-assurance-source.model'; +import { getNotificatioQualityAssuranceRoute } from '../../admin/admin-routing-paths'; + +@Component({ + selector: 'ds-my-dspace-qa-events-notifications', + templateUrl: './my-dspace-qa-events-notifications.component.html', + styleUrls: ['./my-dspace-qa-events-notifications.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class MyDspaceQaEventsNotificationsComponent implements OnInit { + /** + * An Observable that emits an array of QualityAssuranceSourceObject. + */ + sources$: Observable = of([]); + + constructor(private qualityAssuranceSourceDataService: QualityAssuranceSourceDataService) { } + + ngOnInit(): void { + this.getSources(); + } + /** + * Retrieves the sources for Quality Assurance. + * @returns An Observable of the sources for Quality Assurance. + * @throws An error if the retrieval of Quality Assurance sources fails. + */ + getSources() { + this.sources$ = this.qualityAssuranceSourceDataService.getSources() + .pipe( + getFirstCompletedRemoteData(), + getRemoteDataPayload(), + getPaginatedListPayload(), + ); + } + + /** + * Retrieves the quality assurance route. + * @returns The quality assurance route. + */ + getQualityAssuranceRoute(): string { + return getNotificatioQualityAssuranceRoute(); + } +} diff --git a/src/app/notifications/notifications-state.service.spec.ts b/src/app/notifications/notifications-state.service.spec.ts index f07b4f5697..324710ad09 100644 --- a/src/app/notifications/notifications-state.service.spec.ts +++ b/src/app/notifications/notifications-state.service.spec.ts @@ -271,8 +271,8 @@ describe('NotificationsStateService', () => { it('Should call store.dispatch', () => { const elementsPerPage = 3; const currentPage = 1; - const action = new RetrieveAllTopicsAction(elementsPerPage, currentPage); - service.dispatchRetrieveQualityAssuranceTopics(elementsPerPage, currentPage); + const action = new RetrieveAllTopicsAction(elementsPerPage, currentPage, 'source', 'target'); + service.dispatchRetrieveQualityAssuranceTopics(elementsPerPage, currentPage, 'source', 'target'); expect(serviceAsAny.store.dispatch).toHaveBeenCalledWith(action); }); }); diff --git a/src/app/notifications/notifications-state.service.ts b/src/app/notifications/notifications-state.service.ts index c123cfa304..3cdaa589d6 100644 --- a/src/app/notifications/notifications-state.service.ts +++ b/src/app/notifications/notifications-state.service.ts @@ -118,8 +118,8 @@ export class NotificationsStateService { * @param currentPage * The number of the current page. */ - public dispatchRetrieveQualityAssuranceTopics(elementsPerPage: number, currentPage: number): void { - this.store.dispatch(new RetrieveAllTopicsAction(elementsPerPage, currentPage)); + public dispatchRetrieveQualityAssuranceTopics(elementsPerPage: number, currentPage: number, sourceId: string, targteId?: string): void { + this.store.dispatch(new RetrieveAllTopicsAction(elementsPerPage, currentPage, sourceId, targteId)); } // Quality Assurance source diff --git a/src/app/notifications/notifications.module.ts b/src/app/notifications/notifications.module.ts index 00c7582b2f..88b69f26a1 100644 --- a/src/app/notifications/notifications.module.ts +++ b/src/app/notifications/notifications.module.ts @@ -26,6 +26,7 @@ import { QualityAssuranceSourceService } from './qa/source/quality-assurance-sou import { QualityAssuranceSourceDataService } from '../core/notifications/qa/source/quality-assurance-source-data.service'; +import { EPersonDataComponent } from './qa/events/ePerson-data/ePerson-data.component'; import { PublicationClaimComponent } from '../suggestion-notifications/suggestion-targets/publication-claim/publication-claim.component'; import { SuggestionActionsComponent } from '../suggestion-notifications/suggestion-actions/suggestion-actions.component'; import { @@ -65,6 +66,7 @@ const COMPONENTS = [ QualityAssuranceTopicsComponent, QualityAssuranceEventsComponent, QualityAssuranceSourceComponent, + EPersonDataComponent, PublicationClaimComponent, SuggestionActionsComponent, SuggestionListElementComponent, @@ -100,7 +102,7 @@ const PROVIDERS = [ declarations: [ ...COMPONENTS, ...DIRECTIVES, - ...ENTRY_COMPONENTS + ...ENTRY_COMPONENTS, ], providers: [ ...PROVIDERS @@ -110,7 +112,7 @@ const PROVIDERS = [ ], exports: [ ...COMPONENTS, - ...DIRECTIVES + ...DIRECTIVES, ] }) diff --git a/src/app/notifications/qa/events/ePerson-data/ePerson-data.component.html b/src/app/notifications/qa/events/ePerson-data/ePerson-data.component.html new file mode 100644 index 0000000000..058457fd40 --- /dev/null +++ b/src/app/notifications/qa/events/ePerson-data/ePerson-data.component.html @@ -0,0 +1,10 @@ + + + + + {{ ePersonData[property] }} + +
+
+
+
diff --git a/src/app/notifications/qa/events/ePerson-data/ePerson-data.component.scss b/src/app/notifications/qa/events/ePerson-data/ePerson-data.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/notifications/qa/events/ePerson-data/ePerson-data.component.spec.ts b/src/app/notifications/qa/events/ePerson-data/ePerson-data.component.spec.ts new file mode 100644 index 0000000000..6fad8dbc92 --- /dev/null +++ b/src/app/notifications/qa/events/ePerson-data/ePerson-data.component.spec.ts @@ -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; + 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); + }); +}); diff --git a/src/app/notifications/qa/events/ePerson-data/ePerson-data.component.ts b/src/app/notifications/qa/events/ePerson-data/ePerson-data.component.ts new file mode 100644 index 0000000000..f1a9c8c592 --- /dev/null +++ b/src/app/notifications/qa/events/ePerson-data/ePerson-data.component.ts @@ -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 { + if (this.ePersonId) { + return this.ePersonDataService.findById(this.ePersonId, true).pipe( + getFirstCompletedRemoteData(), + getRemoteDataPayload() + ); + } + } +} diff --git a/src/app/notifications/qa/events/quality-assurance-events.component.html b/src/app/notifications/qa/events/quality-assurance-events.component.html index d87ff1b353..8610ae2e84 100644 --- a/src/app/notifications/qa/events/quality-assurance-events.component.html +++ b/src/app/notifications/qa/events/quality-assurance-events.component.html @@ -33,12 +33,17 @@ {{'quality-assurance.event.table.trust' | translate}} {{'quality-assurance.event.table.publication' | translate}} - - {{'quality-assurance.event.table.details' | translate}} - {{'quality-assurance.event.table.project-details' | translate}} + + + {{'quality-assurance.event.table.reasons' | translate}} + + + {{'quality-assurance.event.table.person-who-requested' | translate}} + + {{'quality-assurance.event.table.actions' | translate}} @@ -62,7 +67,8 @@

-

{{'quality-assurance.event.table.subjectValue' | translate}}
{{eventElement.event.message.value}}

+

{{'quality-assurance.event.table.subjectValue' | translate}} +
{{eventElement.event.message.value}}

@@ -75,6 +81,23 @@ {{ (showMore ? 'quality-assurance.event.table.less': 'quality-assurance.event.table.more') | translate }} + + +

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

+ + +

+ + + +

+ + +

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

-
+
+
+ +
@@ -164,14 +197,6 @@
- @@ -225,3 +250,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 3349dd3154..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 @@ -42,6 +42,8 @@ import { SortDirection, SortOptions } from '../../../core/cache/models/sort-opti 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; @@ -118,6 +120,8 @@ describe('QualityAssuranceEventsComponent test suite', () => { { provide: NotificationsService, useValue: new NotificationsServiceStub() }, { 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 c22c28f41e..60550a8baf 100644 --- a/src/app/notifications/qa/events/quality-assurance-events.component.ts +++ b/src/app/notifications/qa/events/quality-assurance-events.component.ts @@ -30,7 +30,10 @@ 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 {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. @@ -76,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} @@ -86,6 +94,7 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy { * @type {Observable} */ public isEventPageLoading: BehaviorSubject = new BehaviorSubject(false); + /** * The modal reference. * @type {any} @@ -110,6 +119,11 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy { */ protected subs: Subscription[] = []; + /** + * Observable that emits a boolean value indicating whether the user is an admin. + */ + isAdmin$: Observable; + /** * Initialize the component variables. * @param {ActivatedRoute} activatedRoute @@ -125,7 +139,8 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy { private notificationsService: NotificationsService, private qualityAssuranceEventRestService: QualityAssuranceEventDataService, private paginationService: PaginationService, - private translateService: TranslateService + private translateService: TranslateService, + private authorizationService: AuthorizationDataService, ) { } @@ -134,12 +149,13 @@ 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; @@ -147,10 +163,17 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy { this.topic = id; 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); + } + } + ); } /** @@ -160,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 ); } @@ -244,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) { @@ -362,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([]); @@ -431,4 +462,13 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy { last() ); } + + /** + * Deletes a quality assurance event. + * @param qaEvent The quality assurance event to delete. + * @returns An Observable of RemoteData containing NoContent. + */ + 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/notifications/qa/topics/quality-assurance-topics.component.spec.ts b/src/app/notifications/qa/topics/quality-assurance-topics.component.spec.ts index fd64b82ce7..228dbffd5e 100644 --- a/src/app/notifications/qa/topics/quality-assurance-topics.component.spec.ts +++ b/src/app/notifications/qa/topics/quality-assurance-topics.component.spec.ts @@ -16,7 +16,7 @@ import { NotificationsStateService } from '../../notifications-state.service'; import { cold } from 'jasmine-marbles'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; import { PaginationService } from '../../../core/pagination/pagination.service'; -import { QualityAssuranceTopicsService } from './quality-assurance-topics.service'; +import { ItemDataService } from 'src/app/core/data/item-data.service'; describe('QualityAssuranceTopicsComponent test suite', () => { let fixture: ComponentFixture; @@ -44,14 +44,14 @@ describe('QualityAssuranceTopicsComponent test suite', () => { providers: [ { provide: NotificationsStateService, useValue: mockNotificationsStateService }, { provide: ActivatedRoute, useValue: { data: observableOf(activatedRouteParams), snapshot: { - paramMap: { - get: () => 'openaire', + params: { + sourceId: 'openaire', + targetId: null }, }}}, { provide: PaginationService, useValue: paginationService }, + { provide: ItemDataService, useValue: {} }, QualityAssuranceTopicsComponent, - // tslint:disable-next-line: no-empty - { provide: QualityAssuranceTopicsService, useValue: { setSourceId: (sourceId: string) => { } }} ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents().then(() => { diff --git a/src/app/notifications/qa/topics/quality-assurance-topics.component.ts b/src/app/notifications/qa/topics/quality-assurance-topics.component.ts index 542d36a9ed..bda45c71c8 100644 --- a/src/app/notifications/qa/topics/quality-assurance-topics.component.ts +++ b/src/app/notifications/qa/topics/quality-assurance-topics.component.ts @@ -1,7 +1,7 @@ -import { Component, OnInit } from '@angular/core'; +import { AfterViewInit, Component, OnDestroy, OnInit } from '@angular/core'; import { Observable, Subscription } from 'rxjs'; -import { distinctUntilChanged, take } from 'rxjs/operators'; +import { distinctUntilChanged, map, take, tap } from 'rxjs/operators'; import { SortOptions } from '../../../core/cache/models/sort-options.model'; import { @@ -14,8 +14,12 @@ 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 { ActivatedRoute } from '@angular/router'; -import { QualityAssuranceTopicsService } from './quality-assurance-topics.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../../../core/shared/operators'; +import { Item } from '../../../core/shared/item.model'; +import { getItemPageRoute } from '../../../item-page/item-page-routing-paths'; +import { getNotificatioQualityAssuranceRoute } from '../../../admin/admin-routing-paths'; /** * Component to display the Quality Assurance topic list. @@ -25,7 +29,7 @@ import { QualityAssuranceTopicsService } from './quality-assurance-topics.servic templateUrl: './quality-assurance-topics.component.html', 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. * @type {PaginationComponentOptions} @@ -60,6 +64,17 @@ export class QualityAssuranceTopicsComponent implements OnInit { */ public sourceId: string; + /** + * This property represents a targetId (item-id) which is used to retrive a topic + * @type {string} + */ + public targetId: string; + + /** + * The URL of the item page. + */ + public itemPageUrl: string; + /** * Initialize the component variables. * @param {PaginationService} paginationService @@ -70,18 +85,27 @@ export class QualityAssuranceTopicsComponent implements OnInit { constructor( private paginationService: PaginationService, private activatedRoute: ActivatedRoute, + private itemService: ItemDataService, private notificationsStateService: NotificationsStateService, - private qualityAssuranceTopicsService: QualityAssuranceTopicsService + private router: Router, ) { + this.sourceId = this.activatedRoute.snapshot.params.sourceId; + this.targetId = this.activatedRoute.snapshot.params.targetId; } /** * Component initialization. */ ngOnInit(): void { - this.sourceId = this.activatedRoute.snapshot.paramMap.get('sourceId'); - this.qualityAssuranceTopicsService.setSourceId(this.sourceId); - 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(); } @@ -93,7 +117,7 @@ export class QualityAssuranceTopicsComponent implements OnInit { this.notificationsStateService.isQualityAssuranceTopicsLoaded().pipe( take(1) ).subscribe(() => { - this.getQualityAssuranceTopics(); + this.getQualityAssuranceTopics(this.sourceId, this.targetId); }) ); } @@ -121,15 +145,17 @@ export class QualityAssuranceTopicsComponent implements OnInit { /** * Dispatch the Quality Assurance topics retrival. */ - public getQualityAssuranceTopics(): void { - this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig).pipe( + public getQualityAssuranceTopics(source: string, target?: string): void { + this.subs.push(this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig).pipe( distinctUntilChanged(), ).subscribe((options: PaginationComponentOptions) => { this.notificationsStateService.dispatchRetrieveQualityAssuranceTopics( options.pageSize, - options.currentPage + options.currentPage, + source, + target ); - }); + })); } /** @@ -150,6 +176,40 @@ export class QualityAssuranceTopicsComponent implements OnInit { } } + /** + * 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. + */ + 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')) + ); + } + + /** + * 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. + */ + getItemPageRoute(item: Item): string { + return getItemPageRoute(item); + } + + /** + * Returns the quality assurance route. + * @returns The quality assurance route. + */ + getQualityAssuranceRoute(): string { + return getNotificatioQualityAssuranceRoute(); + } + /** * Unsubscribe from all subscriptions. */ diff --git a/src/app/notifications/qa/topics/quality-assurance-topics.effects.ts b/src/app/notifications/qa/topics/quality-assurance-topics.effects.ts index a7b4dddd62..830f10c323 100644 --- a/src/app/notifications/qa/topics/quality-assurance-topics.effects.ts +++ b/src/app/notifications/qa/topics/quality-assurance-topics.effects.ts @@ -37,7 +37,9 @@ export class QualityAssuranceTopicsEffects { switchMap(([action, currentState]: [RetrieveAllTopicsAction, any]) => { return this.qualityAssuranceTopicService.getTopics( action.payload.elementsPerPage, - action.payload.currentPage + action.payload.currentPage, + action.payload.source, + action.payload.target ).pipe( map((topics: PaginatedList) => new AddTopicsAction(topics.page, topics.totalPages, topics.currentPage, topics.totalElements) diff --git a/src/app/notifications/qa/topics/quality-assurance-topics.reducer.spec.ts b/src/app/notifications/qa/topics/quality-assurance-topics.reducer.spec.ts index a1c002d3f2..37d83f6e4f 100644 --- a/src/app/notifications/qa/topics/quality-assurance-topics.reducer.spec.ts +++ b/src/app/notifications/qa/topics/quality-assurance-topics.reducer.spec.ts @@ -29,7 +29,7 @@ describe('qualityAssuranceTopicsReducer test suite', () => { const expectedState = qualityAssuranceTopicInitialState; expectedState.processing = true; - const action = new RetrieveAllTopicsAction(elementPerPage, currentPage); + const action = new RetrieveAllTopicsAction(elementPerPage, currentPage, 'ENRICH!MORE!ABSTRACT'); const newState = qualityAssuranceTopicsReducer(qualityAssuranceTopicInitialState, action); expect(newState).toEqual(expectedState); diff --git a/src/app/notifications/qa/topics/quality-assurance-topics.service.spec.ts b/src/app/notifications/qa/topics/quality-assurance-topics.service.spec.ts index c6aae27a88..78cedb2e2a 100644 --- a/src/app/notifications/qa/topics/quality-assurance-topics.service.spec.ts +++ b/src/app/notifications/qa/topics/quality-assurance-topics.service.spec.ts @@ -42,30 +42,30 @@ describe('QualityAssuranceTopicsService', () => { beforeEach(() => { restService = TestBed.inject(QualityAssuranceTopicDataService); restServiceAsAny = restService; - restServiceAsAny.getTopics.and.returnValue(observableOf(paginatedListRD)); + restServiceAsAny.searchTopicsBySource.and.returnValue(observableOf(paginatedListRD)); + restServiceAsAny.searchTopicsByTarget.and.returnValue(observableOf(paginatedListRD)); service = new QualityAssuranceTopicsService(restService); serviceAsAny = service; }); describe('getTopics', () => { - it('Should proxy the call to qualityAssuranceTopicRestService.getTopics', () => { + it('should proxy the call to qualityAssuranceTopicRestService.searchTopicsBySource', () => { const sortOptions = new SortOptions('name', SortDirection.ASC); const findListOptions: FindListOptions = { elementsPerPage: elementsPerPage, currentPage: currentPage, sort: sortOptions, - searchParams: [new RequestParam('source', 'ENRICH!MORE!ABSTRACT')] + searchParams: [new RequestParam('source', 'openaire')] }; - service.setSourceId('ENRICH!MORE!ABSTRACT'); - const result = service.getTopics(elementsPerPage, currentPage); - expect((service as any).qualityAssuranceTopicRestService.getTopics).toHaveBeenCalledWith(findListOptions); + service.getTopics(elementsPerPage, currentPage, 'openaire'); + expect((service as any).qualityAssuranceTopicRestService.searchTopicsBySource).toHaveBeenCalledWith(findListOptions); }); - it('Should return a paginated list of Quality Assurance topics', () => { + it('should return a paginated list of Quality Assurance topics', () => { const expected = cold('(a|)', { a: paginatedList }); - const result = service.getTopics(elementsPerPage, currentPage); + const result = service.getTopics(elementsPerPage, currentPage, 'openaire'); expect(result).toBeObservable(expected); }); }); diff --git a/src/app/notifications/qa/topics/quality-assurance-topics.service.ts b/src/app/notifications/qa/topics/quality-assurance-topics.service.ts index 9dd581ebed..131be400ca 100644 --- a/src/app/notifications/qa/topics/quality-assurance-topics.service.ts +++ b/src/app/notifications/qa/topics/quality-assurance-topics.service.ts @@ -13,6 +13,7 @@ import { import { RequestParam } from '../../../core/cache/models/request-param.model'; import { FindListOptions } from '../../../core/data/find-list-options.model'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; +import { hasValue } from '../../../shared/empty.util'; /** * The service handling all Quality Assurance topic requests to the REST service. @@ -28,10 +29,6 @@ export class QualityAssuranceTopicsService { private qualityAssuranceTopicRestService: QualityAssuranceTopicDataService ) { } - /** - * sourceId used to get topics - */ - sourceId: string; /** * Return the list of Quality Assurance topics managing pagination and errors. @@ -43,17 +40,25 @@ export class QualityAssuranceTopicsService { * @return Observable> * The list of Quality Assurance topics. */ - public getTopics(elementsPerPage, currentPage): Observable> { + public getTopics(elementsPerPage, currentPage, source: string, target?: string): Observable> { const sortOptions = new SortOptions('name', SortDirection.ASC); - const findListOptions: FindListOptions = { elementsPerPage: elementsPerPage, currentPage: currentPage, sort: sortOptions, - searchParams: [new RequestParam('source', this.sourceId)] + searchParams: [new RequestParam('source', source)] }; - return this.qualityAssuranceTopicRestService.getTopics(findListOptions).pipe( + let request$: Observable>>; + + if (hasValue(target)) { + findListOptions.searchParams.push(new RequestParam('target', target)); + request$ = this.qualityAssuranceTopicRestService.searchTopicsByTarget(findListOptions); + } else { + request$ = this.qualityAssuranceTopicRestService.searchTopicsBySource(findListOptions); + } + + return request$.pipe( getFirstCompletedRemoteData(), map((rd: RemoteData>) => { if (rd.hasSucceeded) { @@ -64,12 +69,4 @@ export class QualityAssuranceTopicsService { }) ); } - - /** - * set sourceId which is used to get topics - * @param sourceId string - */ - setSourceId(sourceId: string) { - this.sourceId = sourceId; - } } diff --git a/src/app/shared/correction-suggestion/item-withdrawn-reinstate-modal.component.html b/src/app/shared/correction-suggestion/item-withdrawn-reinstate-modal.component.html new file mode 100644 index 0000000000..1463235bee --- /dev/null +++ b/src/app/shared/correction-suggestion/item-withdrawn-reinstate-modal.component.html @@ -0,0 +1,55 @@ +
+ + + +
+ + + + + + + + + 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 707b9a9e6a..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', { - getTopics: jasmine.createSpy('getTopics'), 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 f8fb2fbb32..e31ff2cac1 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -279,8 +279,11 @@ import { } from '../item-page/simple/field-components/specific-field/title/themed-item-page-field.component'; import { BitstreamListItemComponent } from './object-list/bitstream-list-item/bitstream-list-item.component'; 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 { 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'; @@ -322,7 +325,8 @@ const PIPES = [ ObjNgFor, BrowserOnlyPipe, MarkdownPipe, - ShortNumberPipe + ShortNumberPipe, + SplitPipe ]; const COMPONENTS = [ @@ -472,7 +476,9 @@ const ENTRY_COMPONENTS = [ const PROVIDERS = [ TruncatableService, MockAdminGuard, - AbstractTrackableComponent + AbstractTrackableComponent, + QualityAssuranceEventDataService, + QualityAssuranceSourceDataService ]; const DIRECTIVES = [ diff --git a/src/app/shared/utils/split.pipe.ts b/src/app/shared/utils/split.pipe.ts new file mode 100644 index 0000000000..e4d0f2cc49 --- /dev/null +++ b/src/app/shared/utils/split.pipe.ts @@ -0,0 +1,16 @@ +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/app/suggestion-notifications/suggestions.service.ts b/src/app/suggestion-notifications/suggestions.service.ts index 3b2ce081f1..8d70bb9d5f 100644 --- a/src/app/suggestion-notifications/suggestions.service.ts +++ b/src/app/suggestion-notifications/suggestions.service.ts @@ -11,9 +11,10 @@ import { hasValue, isNotEmpty } from '../shared/empty.util'; import { ResearcherProfile } from '../core/profile/model/researcher-profile.model'; import { getAllSucceededRemoteDataPayload, - getFinishedRemoteData, getFirstCompletedRemoteData, + getFinishedRemoteData, + getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload, - getFirstSucceededRemoteListPayload + getFirstSucceededRemoteListPayload, } from '../core/shared/operators'; import { Suggestion } from '../core/suggestion-notifications/models/suggestion.model'; import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index a133cf5a3e..71a1c35e3f 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}}", @@ -3218,6 +3268,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", @@ -3232,7 +3284,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.", @@ -3260,12 +3314,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", @@ -3294,6 +3352,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", @@ -3306,6 +3366,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 ad2478ae61..0960b9fae1 100644 --- a/src/assets/i18n/it.json5 +++ b/src/assets/i18n/it.json5 @@ -3535,6 +3535,22 @@ // "item.truncatable-part.show-less": "Collapse", "item.truncatable-part.show-less": "Riduci", + // "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 suggestions related to your account", + + // "item.qa-event-notification-info.check.button": "View", + // TODO New key - Add a translation + "item.qa-event-notification-info.check.button": "View", + + // "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 suggestions related to your account", + + // "mydspace.qa-event-notification-info.check.button": "View", + // TODO New key - Add a translation + "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", 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/assets/images/qa-coar-notify-logo.png b/src/assets/images/qa-coar-notify-logo.png new file mode 100644 index 0000000000..0ba021dfd2 Binary files /dev/null and b/src/assets/images/qa-coar-notify-logo.png differ diff --git a/src/assets/images/qa-openaire-logo.png b/src/assets/images/qa-openaire-logo.png new file mode 100644 index 0000000000..359fd73b84 Binary files /dev/null and b/src/assets/images/qa-openaire-logo.png differ diff --git a/src/styles/_custom_variables.scss b/src/styles/_custom_variables.scss index 7d2f80d451..ba49ae2114 100644 --- a/src/styles/_custom_variables.scss +++ b/src/styles/_custom_variables.scss @@ -108,6 +108,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 d2bac663b4..1838cf794d 100644 --- a/src/themes/custom/lazy-theme.module.ts +++ b/src/themes/custom/lazy-theme.module.ts @@ -305,7 +305,7 @@ const DECLARATIONS = [ NgxGalleryModule, FormModule, RequestCopyModule, - NotificationsModule + NotificationsModule, ], declarations: DECLARATIONS, })