Merge pull request #2759 from 4Science/CST-12109-WITHDRAWN-REINSTATE-requests

WITHDRAW / REINSTATE requests for an item
This commit is contained in:
Tim Donohue
2024-02-27 16:07:43 -06:00
committed by GitHub
67 changed files with 1528 additions and 199 deletions

View File

@@ -1,9 +1,7 @@
import { URLCombiner } from '../../core/url-combiner/url-combiner';
import { getNotificationsModuleRoute } from '../admin-routing-paths';
export const QUALITY_ASSURANCE_EDIT_PATH = 'quality-assurance';
export const 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}`;
}

View File

@@ -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',

View File

@@ -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();
}

View File

@@ -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]
},
])
],

View File

@@ -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';

View File

@@ -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 }
]
}
], {

View File

@@ -33,7 +33,7 @@ export class QualityAssuranceBreadcrumbService implements BreadcrumbsProviderSer
*/
getBreadcrumbs(key: string, url: string): Observable<Breadcrumb[]> {
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 {

View File

@@ -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
];
/**

View File

@@ -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<Qu
return this.searchData.searchBy('findByTopic', options, true, true, ...linksToFollow);
}
/**
* Service for retrieving Quality Assurance events by topic and target.
* @param options (Optional) The search options to use when retrieving the events.
* @param linksToFollow (Optional) The links to follow when retrieving the events.
* @returns An observable of the remote data containing the paginated list of Quality Assurance events.
*/
public searchEventsByTopic(options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<QualityAssuranceEventObject>[]): Observable<RemoteData<PaginatedList<QualityAssuranceEventObject>>> {
return this.searchData.searchBy('findByTopic', options, true, true, ...linksToFollow);
}
/**
* Clear findByTopic requests from cache
*/
@@ -200,4 +215,38 @@ export class QualityAssuranceEventDataService extends IdentifiableDataService<Qu
return this.rdbService.buildFromRequestUUID<QualityAssuranceEventObject>(requestId);
}
/**
* Perform a post on an endpoint related to correction type
* @param data the data to post
* @returns the RestResponse as an Observable
*/
postData(target: string, correctionType: string, related: string, reason: string): Observable<RemoteData<QualityAssuranceEventObject>> {
const requestId = this.requestService.generateRequestId();
const href$ = this.getBrowseEndpoint();
return href$.pipe(
switchMap((href: string) => {
const options: HttpOptions = Object.create({});
let headers = new HttpHeaders();
headers = headers.append('Content-Type', 'application/json');
options.headers = headers;
let params = new HttpParams();
params = params.append('target', target)
.append('correctionType', correctionType);
options.params = params;
const request = new PostRequest(requestId, href, {'reason': reason} , options);
if (hasValue(this.responseMsToLive)) {
request.responseMsToLive = this.responseMsToLive;
}
this.requestService.send(request);
return this.rdbService.buildFromRequestUUID<QualityAssuranceEventObject>(requestId);
})
);
}
public deleteQAEvent(qaEvent: QualityAssuranceEventData): Observable<RemoteData<NoContent>> {
return this.deleteData.delete(qaEvent.id);
}
}

View File

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

View File

@@ -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<QualityAssuranceSourceObject> {
private findAllData: FindAllData<QualityAssuranceSourceObject>;
private searchAllData: SearchData<QualityAssuranceSourceObject>;
private searchByTargetMethod = 'byTarget';
/**
* Initialize service variables
@@ -43,6 +47,7 @@ export class QualityAssuranceSourceDataService extends IdentifiableDataService<Q
) {
super('qualityassurancesources', requestService, rdbService, objectCache, halService);
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
this.searchAllData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
}
/**
@@ -84,4 +89,16 @@ export class QualityAssuranceSourceDataService extends IdentifiableDataService<Q
public getSource(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<QualityAssuranceSourceObject>[]): Observable<RemoteData<QualityAssuranceSourceObject>> {
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<QualityAssuranceSourceObject>[]): Observable<RemoteData<PaginatedList<QualityAssuranceSourceObject>>> {
return this.searchAllData.searchBy(this.searchByTargetMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
}

View File

@@ -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<PaginatedList<QualityAssuranceTopicObject>> for the object with the given URL', () => {
const result = service.getTopics();
const result = service.searchTopicsByTarget();
const expected = cold('(a)', {
a: paginatedListRD
});

View File

@@ -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<QualityAssuranceTopicObject> {
private findAllData: FindAllData<QualityAssuranceTopicObject>;
private searchData: SearchData<QualityAssuranceTopicObject>;
private searchByTargetMethod = 'byTarget';
private searchBySourceMethod = 'bySource';
/**
* Initialize service variables
@@ -43,23 +48,31 @@ export class QualityAssuranceTopicDataService extends IdentifiableDataService<Qu
) {
super('qualityassurancetopics', requestService, rdbService, objectCache, halService);
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
}
/**
* Return the list of Quality Assurance topics.
*
* @param options Find list options object.
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved.
*
* @return Observable<RemoteData<PaginatedList<QualityAssuranceTopicObject>>>
* 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<QualityAssuranceTopicObject>[]): Observable<RemoteData<PaginatedList<QualityAssuranceTopicObject>>> {
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
public searchTopicsByTarget(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<QualityAssuranceTopicObject>[]): Observable<RemoteData<PaginatedList<QualityAssuranceTopicObject>>> {
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<QualityAssuranceTopicObject>[]): Observable<RemoteData<PaginatedList<QualityAssuranceTopicObject>>> {
return this.searchData.searchBy(this.searchBySourceMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
/**

View File

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

View File

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

View File

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

View File

@@ -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<ItemAlertsComponent>;
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);
});
});
});
});

View File

@@ -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<boolean> {
const correction$ = this.correctionTypeDataService.findByItem(this.item.uuid, true).pipe(
getFirstCompletedRemoteData(),
getRemoteDataPayload(),
getPaginatedListPayload()
);
const isAdmin$ = this.authService.isAuthorized(FeatureID.AdministratorOf);
return combineLatest([isAdmin$, correction$]).pipe(
map(([isAdmin, correction]) => {
return !isAdmin && correction.some((correctionType) => correctionType.topic === REQUEST_REINSTATE);
}
));
}
/**
* Opens the reinstate modal for the item.
*/
openReinstateModal() {
this.dsoWithdrawnReinstateModalService.openCreateWithdrawnReinstateModal(this.item, 'request-reinstate', this.item.isArchived);
}
}

View File

@@ -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({

View File

@@ -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
];

View File

@@ -2,6 +2,7 @@
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
<div *ngIf="itemRD?.payload as item">
<ds-themed-item-alerts [item]="item"></ds-themed-item-alerts>
<ds-qa-event-notification [item]="item"></ds-qa-event-notification>
<ds-item-versions-notice [item]="item"></ds-item-versions-notice>
<ds-view-tracker [object]="item"></ds-view-tracker>
<ds-listable-object-component-loader *ngIf="!item.isWithdrawn || (isAdmin$|async)" [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>

View File

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

View File

@@ -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;
}

View File

@@ -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<QaEventNotificationComponent>;
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');
});
});

View File

@@ -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<QualityAssuranceSourceObject[]>;
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:<target>"
*/
getQualityAssuranceSources$(): Observable<QualityAssuranceSourceObject[]> {
const findListTopicOptions: FindListOptions = {
searchParams: [new RequestParam('target', this.item.uuid)]
};
return this.qualityAssuranceSourceDataService.getSourcesByTarget(findListTopicOptions, false)
.pipe(
getFirstCompletedRemoteData(),
map((data: RemoteData<PaginatedList<QualityAssuranceSourceObject>>) => {
if (data.hasSucceeded) {
return data.payload.page;
}
return [];
}),
catchError(() => [])
);
}
/**
* Returns the quality assurance route.
* @returns The quality assurance route.
*/
getQualityAssuranceRoute(): string {
return getNotificatioQualityAssuranceRoute();
}
}

View File

@@ -171,7 +171,8 @@ export class MenuResolver implements Resolve<boolean> {
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<boolean> {
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<boolean> {
* 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',

View File

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

View File

@@ -15,6 +15,9 @@ import { MyDSpaceNewExternalDropdownComponent } from './my-dspace-new-submission
import { ThemedMyDSpacePageComponent } from './themed-my-dspace-page.component';
import { SearchModule } from '../shared/search/search.module';
import { UploadModule } from '../shared/upload/upload.module';
import {
MyDspaceQaEventsNotificationsComponent
} from './my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component';
import { NotificationsModule } from '../notifications/notifications.module';
const DECLARATIONS = [
@@ -23,7 +26,8 @@ const DECLARATIONS = [
MyDSpaceNewSubmissionComponent,
CollectionSelectorComponent,
MyDSpaceNewSubmissionDropdownComponent,
MyDSpaceNewExternalDropdownComponent
MyDSpaceNewExternalDropdownComponent,
MyDspaceQaEventsNotificationsComponent
];
@NgModule({

View File

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

View File

@@ -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;
}

View File

@@ -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<QualityAssuranceSourceObject[]> = 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();
}
}

View File

@@ -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);
});
});

View File

@@ -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

View File

@@ -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,
]
})

View File

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

View File

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

View File

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

View File

@@ -33,12 +33,17 @@
<tr>
<th scope="col" class="trust-col">{{'quality-assurance.event.table.trust' | translate}}</th>
<th scope="col" class="title-col">{{'quality-assurance.event.table.publication' | translate}}</th>
<th *ngIf="hasDetailColumn() && showTopic.indexOf('/PROJECT') == -1" scope="col" class="content-col">
{{'quality-assurance.event.table.details' | translate}}
</th>
<th *ngIf="hasDetailColumn() && showTopic.indexOf('/PROJECT') !== -1" scope="col" class="content-col">
{{'quality-assurance.event.table.project-details' | translate}}
</th>
<ng-container *ngIf="hasDetailColumn() && (showTopic.indexOf('/REINSTATE') !== -1 || showTopic.indexOf('/WITHDRAWN') !== -1)">
<th scope="col" class="content-col">
{{'quality-assurance.event.table.reasons' | translate}}
</th>
<th scope="col" class="content-col">
{{'quality-assurance.event.table.person-who-requested' | translate}}
</th>
</ng-container>
<th scope="col" class="button-col">{{'quality-assurance.event.table.actions' | translate}}</th>
</tr>
</thead>
@@ -62,7 +67,8 @@
</p>
</td>
<td *ngIf="showTopic.indexOf('/SUBJECT') !== -1">
<p><span class="small">{{'quality-assurance.event.table.subjectValue' | translate}}</span><br><span class="badge badge-info">{{eventElement.event.message.value}}</span></p>
<p><span class="small">{{'quality-assurance.event.table.subjectValue' | translate}}
</span><br><span class="badge badge-info">{{eventElement.event.message.value}}</span></p>
</td>
<td *ngIf="showTopic.indexOf('/ABSTRACT') !== -1">
<p class="abstract-container" [class.show]="showMore">
@@ -75,6 +81,23 @@
{{ (showMore ? 'quality-assurance.event.table.less': 'quality-assurance.event.table.more') | translate }}
</button>
</td>
<ng-container *ngIf="showTopic.indexOf('/REINSTATE') !== -1 || showTopic.indexOf('/WITHDRAWN') !== -1">
<td>
<p>
<span *ngIf="eventElement.event.message">
<span>{{eventElement.event.message.reason}}</span><br>
</span>
</p>
</td>
<td>
<p>
<span *ngIf="eventElement.event.originalId">
<ds-eperson-data [ePersonId]="eventElement.event.originalId" [properties]="['email']"></ds-eperson-data>
</span>
</p>
</td>
</ng-container>
<td *ngIf="showTopic.indexOf('/PROJECT') !== -1">
<p>
{{'quality-assurance.event.table.suggestedProject' | translate}}
@@ -115,7 +138,7 @@
</div>
</td>
<td>
<div class="btn-group button-width">
<div *ngIf="(isAdmin$ | async)" class="btn-group button-width">
<button *ngIf="showTopic.indexOf('/PROJECT') !== -1"
class="btn btn-outline-success btn-sm button-width"
ngbTooltip="{{'quality-assurance.event.action.import' | translate}}"
@@ -155,6 +178,16 @@
<i class="fas fa-trash-alt"></i>
</button>
</div>
<div *ngIf="!(isAdmin$ | async)" class="btn-group button-width">
<button class="btn btn-outline-danger btn-sm button-width"
ngbTooltip="{{'quality-assurance.event.action.undo' | translate}}"
container="body"
[disabled]="eventElement.isRunning"
[attr.aria-label]="'quality-assurance.event.action.undo' | translate"
(click)="openModal('UNDO', eventElement, undoModal)">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</td>
</tr>
</tbody>
@@ -164,14 +197,6 @@
</ds-pagination>
</div>
</div>
<div class="row text-right">
<div class="col-md-12">
<a class="btn btn-outline-secondary" [routerLink]="['/admin/notifications/quality-assurance']">
<i class="fas fa-angle-double-left"></i>
{{'quality-assurance.events.back' | translate}}
</a>
</div>
</div>
</div>
<ng-template #acceptModal let-modal>
@@ -225,3 +250,20 @@
</button>
</div>
</ng-template>
<ng-template #undoModal let-modal>
<div class="modal-header">
<h4 class="modal-title" id="undoModal">{{'quality-assurance.event.sure' | translate}}</h4>
</div>
<div class="modal-body">
<p>{{'quality-assurance.event.undo.description' | translate}}</p>
<button class="btn btn-outline-danger float-right" (click)="modal.close('do')">
<i class="fas fa-trash-alt"></i>
<span class="d-none d-sm-inline"> {{'quality-assurance.event.action.undo' | translate}}</span>
</button>
<button class="btn btn-outline-secondary" (click)="modal.close('cancel')">
<i class="fas fa-close"></i>
<span class="d-none d-sm-inline"> {{'quality-assurance.event.action.cancel' | translate}}</span>
</button>
</div>
</ng-template>

View File

@@ -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<QualityAssuranceEventsComponent>;
@@ -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]

View File

@@ -30,6 +30,9 @@ 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 { 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';
/**
@@ -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<boolean>}
*/
public isEventPageLoading: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(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<boolean>;
/**
* 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,10 +149,11 @@ 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')];
this.sourceId = params.get('sourceId');
}),
map((params) => params.get('topicId')),
take(1),
@@ -147,10 +163,17 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy {
this.topic = id;
return this.getQualityAssuranceEvents();
})
).subscribe((events: QualityAssuranceEventData[]) => {
).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<QualityAssuranceEventObject>) => {
if (rd.hasSucceeded) {
@@ -362,7 +393,7 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy {
switchMap((rd: RemoteData<PaginatedList<QualityAssuranceEventObject>>) => {
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<RemoteData<NoContent>> {
return this.qualityAssuranceEventRestService.deleteQAEvent(qaEvent);
}
}

View File

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

View File

@@ -25,6 +25,8 @@ export class RetrieveAllTopicsAction implements Action {
payload: {
elementsPerPage: number;
currentPage: number;
source: string;
target?: string;
};
/**
@@ -35,10 +37,12 @@ export class RetrieveAllTopicsAction implements Action {
* @param currentPage
* The page number to retrieve
*/
constructor(elementsPerPage: number, currentPage: number) {
constructor(elementsPerPage: number, currentPage: number, source: string, target?: string) {
this.payload = {
elementsPerPage,
currentPage
currentPage,
source,
target
};
}
}

View File

@@ -2,7 +2,11 @@
<div class="row">
<div class="col-12">
<h2 class="border-bottom pb-2">{{'quality-assurance.title'| translate}}</h2>
<ds-alert [type]="'alert-info'">{{'quality-assurance.topics.description'| translate:{source: sourceId} }}</ds-alert>
<ds-alert *ngIf="!targetId" [type]="'alert-info'">{{'quality-assurance.topics.description'| translate:{source: sourceId} }}</ds-alert>
<ds-alert *ngIf="targetId" [type]="'alert-info'">
{{'quality-assurance.topics.description-with-target'| translate:{source: sourceId} }}
<a [routerLink]="itemPageUrl">{{(getTargetItemTitle() | async)}}</a>
</ds-alert>
</div>
</div>
<div class="row">
@@ -15,7 +19,7 @@
[collectionSize]="(totalElements$ | async)"
[hideGear]="false"
[hideSortOptions]="true"
(paginationChange)="getQualityAssuranceTopics()">
(paginationChange)="getQualityAssuranceTopics(sourceId, targetId)">
<ds-loading class="container" *ngIf="(isTopicsProcessing() | async)" message="'quality-assurance.loading' | translate"></ds-loading>
<ng-container *ngIf="!(isTopicsProcessing() | async)">
@@ -34,13 +38,13 @@
<tbody>
<tr *ngFor="let topicElement of (topics$ | async); let i = index">
<td>{{topicElement.name}}</td>
<td>{{topicElement.lastEvent}}</td>
<td>{{topicElement.lastEvent | date: 'dd/MM/yyyy hh:mm' }}</td>
<td>
<div class="btn-group edit-field">
<button
class="btn btn-outline-primary btn-sm"
title="{{'quality-assurance.button.detail' | translate }}"
[routerLink]="[topicElement.id]">
title="{{'quality-assurance.topics-list.button.detail' | translate : { param: topicElement.name } }}"
[routerLink]="[getQualityAssuranceRoute(), sourceId, topicElement.id]">
<span class="badge badge-info">{{topicElement.totalEvents}}</span>
<i class="fas fa-info fa-fw"></i>
</button>

View File

@@ -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<QualityAssuranceTopicsComponent>;
@@ -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(() => {

View File

@@ -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<string> {
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.
*/

View File

@@ -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<QualityAssuranceTopicObject>) =>
new AddTopicsAction(topics.page, topics.totalPages, topics.currentPage, topics.totalElements)

View File

@@ -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);

View File

@@ -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);
});
});

View File

@@ -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<PaginatedList<QualityAssuranceTopicObject>>
* The list of Quality Assurance topics.
*/
public getTopics(elementsPerPage, currentPage): Observable<PaginatedList<QualityAssuranceTopicObject>> {
public getTopics(elementsPerPage, currentPage, source: string, target?: string): Observable<PaginatedList<QualityAssuranceTopicObject>> {
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<RemoteData<PaginatedList<QualityAssuranceTopicObject>>>;
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<PaginatedList<QualityAssuranceTopicObject>>) => {
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;
}
}

View File

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

View File

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

View File

@@ -23,6 +23,10 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { Community } from '../../core/shared/community.model';
import { 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();
});
});
});

View File

@@ -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<MenuSection[]> {
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;
});
}
}

View File

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

View File

@@ -1482,7 +1482,8 @@ export const qualityAssuranceEventObjectMissingPid: QualityAssuranceEventObject
funder: null,
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'),
});
}

View File

@@ -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 = [

View File

@@ -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);
}
}

View File

@@ -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';

View File

@@ -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.",

View File

@@ -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",

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -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;

View File

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