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 f92a96d2422..9fcabedd647 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 07a98aa080d..e00a88cbe22 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 30f801cecb7..144b9d09bf0 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 a7d19a69357..c17dd5554ff 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 fe2837c6e3f..2f52c643667 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 a14000aef4d..e94ec512155 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 209ae0722ce..53aba9fa0d9 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 f151f10f661..119b993faf9 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 7f7e68afaab..e266ace080b 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 0cdb4a57456..1d66e5bb28a 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 03a5da2e8c4..f6a58fdd45f 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 360e6b1ccd4..bade6cace57 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 2bf5195bf1e..919aaac71a9 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 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 getTopics(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { - return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + 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 00000000000..8a5bbb1fb8f --- /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 00000000000..9329fa88d8a --- /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 cd71d23a910..f6304340f3d 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 a933eb6a589..47a4852cf19 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 2b1df58c9f7..025dafb425e 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 a8d41d15352..5aa1b6e508f 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 8b7243acde0..49f575cbb3d 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 cc9983bb354..37a5e0c4cbf 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 00000000000..77370f462d8 --- /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 00000000000..2a62342b7c9 --- /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 00000000000..ce231affeea --- /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 00000000000..1557a65a0e6 --- /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 fc6eb00195c..9272ed06ea3 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 c5e49b0cece..cfae8e07a86 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 00000000000..2a62342b7c9 --- /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 00000000000..63e10bb5657 --- /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 f07b4f56970..324710ad09b 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 c123cfa3047..3cdaa589d62 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 00c7582b2f0..88b69f26a14 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 00000000000..058457fd40c --- /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 00000000000..e69de29bb2d 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 00000000000..6fad8dbc922 --- /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 00000000000..f1a9c8c5928 --- /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 d87ff1b3531..8610ae2e84c 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 @@