[CST-12109] fixes

This commit is contained in:
Alisa Ismailati
2024-02-05 19:37:08 +01:00
parent 1b5007aae0
commit f916bd7776
20 changed files with 225 additions and 133 deletions

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

@@ -8,7 +8,7 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv
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/correction-type-mode.model';
import { CorrectionType } from './models/correctiontype.model';
import { Observable, map } from 'rxjs';
import { RemoteData } from '../data/remote-data';
import { PaginatedList } from '../data/paginated-list.model';

View File

@@ -6,6 +6,10 @@ 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');
@@ -15,20 +19,30 @@ export class CorrectionType extends CacheableObject {
@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

@@ -1,4 +1,4 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing';
import { ItemAlertsComponent } from './item-alerts.component';
import { TranslateModule } from '@ngx-translate/core';
import { NO_ERRORS_SCHEMA } from '@angular/core';
@@ -24,7 +24,7 @@ describe('ItemAlertsComponent', () => {
beforeEach(waitForAsync(() => {
authorizationService = jasmine.createSpyObj('authorizationService', ['isAuthorized']);
dsoWithdrawnReinstateModalService = jasmine.createSpyObj('dsoWithdrawnReinstateModalService',['openCreateWithdrawnReinstateModal']);
dsoWithdrawnReinstateModalService = jasmine.createSpyObj('dsoWithdrawnReinstateModalService', ['openCreateWithdrawnReinstateModal']);
correctionTypeDataService = jasmine.createSpyObj('correctionTypeDataService', {
findByItem: of({})
});
@@ -43,6 +43,7 @@ describe('ItemAlertsComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(ItemAlertsComponent);
component = fixture.componentInstance;
component.item = itemMock;
fixture.detectChanges();
@@ -108,46 +109,16 @@ describe('ItemAlertsComponent', () => {
});
});
describe('show reinstate button', () => {
it('should return false if user is an admin', () => {
const isAdmin$ = of(true);
authorizationService.isAuthorized.and.returnValue(isAdmin$);
const result$ = component.showReinstateButton$();
result$.subscribe((result) => {
expect(result).toBe(false);
});
it('should return true when user is not an admin and there is at least one correction with topic REQUEST_REINSTATE', fakeAsync(() => {
const isAdmin = false;
const correction = [{ topic: 'REQUEST_REINSTATE' }];
authorizationService.isAuthorized.and.returnValue(of(isAdmin));
correctionTypeDataService.findByItem.and.returnValue(of(correction));
const result$ = component.showReinstateButton$();
tick();
result$.subscribe((result) => {
expect(result).toBeTrue();
});
it('should return false if no correction types are found', () => {
const isAdmin$ = of(false);
authorizationService.isAuthorized.and.returnValue(isAdmin$);
correctionTypeDataService.findByItem.and.returnValue(of([]));
const result$ = component.showReinstateButton$();
result$.subscribe((result) => {
expect(result).toBe(false);
});
});
it('should return false if no correction type with topic "REQUEST_REINSTATE" is found', () => {
const isAdmin$ = of(false);
authorizationService.isAuthorized.and.returnValue(isAdmin$);
correctionTypeDataService.findByItem.and.returnValue(of([{ topic: 'OTHER_TOPIC' }]));
const result$ = component.showReinstateButton$();
result$.subscribe((result) => {
expect(result).toBe(false);
});
});
it('should return true if user is not an admin and correction type with topic "REQUEST_REINSTATE" is found', () => {
const isAdmin$ = of(false);
authorizationService.isAuthorized.and.returnValue(isAdmin$);
correctionTypeDataService.findByItem.and.returnValue(of([{ topic: 'REQUEST_REINSTATE' }]));
const result$ = component.showReinstateButton$();
result$.subscribe((result) => {
expect(result).toBe(true);
});
});
});
}));
});

View File

@@ -4,12 +4,14 @@
class="alert alert-info d-flex flex-row"
*ngIf="source.totalEvents > 0"
>
<div class="col-2">
<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 } }}

View File

@@ -26,7 +26,7 @@ import { QualityAssuranceSourceService } from './qa/source/quality-assurance-sou
import {
QualityAssuranceSourceDataService
} from '../core/notifications/qa/source/quality-assurance-source-data.service';
import { GetEPersonDataPipe } from './qa/events/get-ePerson-data.pipe';
import { EPersonDataComponent } from './qa/events/ePerson-data/ePerson-data.component';
const MODULES = [
CommonModule,
@@ -41,7 +41,8 @@ const MODULES = [
const COMPONENTS = [
QualityAssuranceTopicsComponent,
QualityAssuranceEventsComponent,
QualityAssuranceSourceComponent
QualityAssuranceSourceComponent,
EPersonDataComponent,
];
const DIRECTIVES = [ ];
@@ -60,7 +61,6 @@ const PROVIDERS = [
];
const PIPES = [
GetEPersonDataPipe
];
@NgModule({

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

@@ -1,24 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
import { Observable, tap } from 'rxjs';
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
import { EPerson } from '../../../core/eperson/models/eperson.model';
import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../../../core/shared/operators';
@Pipe({
name: 'dsGetEPersonData'
})
export class GetEPersonDataPipe implements PipeTransform {
constructor(private ePersonDataService: EPersonDataService) { }
/**
* Transforms the personId into an Observable of EPerson.
* @param personId The ID of the person.
* @returns An Observable of EPerson.
*/
transform(personId: string): Observable<EPerson> {
return this.ePersonDataService.findById(personId, true).pipe(
getFirstCompletedRemoteData(),
getRemoteDataPayload()
);
}
}

View File

@@ -85,16 +85,14 @@
<td>
<p>
<span *ngIf="eventElement.event.message">
<span class="badge badge-info">{{eventElement.event.message.reason}}</span><br>
<span>{{eventElement.event.message.reason}}</span><br>
</span>
</p>
</td>
<td>
<p>
<span *ngIf="eventElement.event.originalId">
<span class="badge badge-info">
{{ (eventElement.event.originalId | dsGetEPersonData | async)?.email }}
</span>
<ds-eperson-data [ePersonId]="eventElement.event.originalId" [properties]="['email']"></ds-eperson-data>
</span>
</p>
</td>
@@ -200,7 +198,7 @@
</div>
<div class="row text-right">
<div class="col-md-12">
<a class="btn btn-outline-secondary" [routerLink]="['/notifications/quality-assurance']">
<a class="btn btn-outline-secondary" [routerLink]="['/notifications/quality-assurance', sourceId]">
<i class="fas fa-angle-double-left"></i>
{{'quality-assurance.events.back' | translate}}
</a>

View File

@@ -34,7 +34,6 @@ import { AuthorizationDataService } from '../../../core/data/feature-authorizati
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { NoContent } from '../../../core/shared/NoContent.model';
import {environment} from '../../../../environments/environment';
import { getEntityPageRoute } from 'src/app/item-page/item-page-routing-paths';
/**
* Component to display the Quality Assurance event list.
@@ -80,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}
@@ -144,9 +148,11 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy {
this.isEventPageLoading.next(true);
this.isAdmin$ = this.authorizationService.isAuthorized(FeatureID.AdministratorOf);
// this.sourceId = this.activatedRoute.snapshot.params.sourceId;
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),
@@ -459,8 +465,4 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy {
delete(qaEvent: QualityAssuranceEventData): Observable<RemoteData<NoContent>> {
return this.qualityAssuranceEventRestService.deleteQAEvent(qaEvent);
}
getEntityPageRoute(itemId: string): string {
return getEntityPageRoute('person', itemId);
}
}

View File

@@ -34,12 +34,12 @@
<tbody>
<tr *ngFor="let sourceElement of (sources$ | async); let i = index">
<td>{{sourceElement.id}}</td>
<td>{{formatDate(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

@@ -8,7 +8,7 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio
import { NotificationsStateService } from '../../notifications-state.service';
import { AdminQualityAssuranceSourcePageParams } from '../../../admin/admin-notifications/admin-quality-assurance-source-page-component/admin-quality-assurance-source-page-resolver.service';
import { hasValue } from '../../../shared/empty.util';
import { format } from 'date-fns';
/**
* Component to display the Quality Assurance source list.
*/
@@ -65,20 +65,6 @@ export class QualityAssuranceSourceComponent implements OnInit {
this.totalElements$ = this.notificationsStateService.getQualityAssuranceSourceTotals();
}
/**
* Formats the given date string into the format 'yyyy-MM-dd HH:mm:ss'.
* If the date is falsy, an empty string is returned.
*
* @param date - The date string to be formatted.
* @returns The formatted date string.
*/
formatDate(date: string): string {
if (!date) {
return '';
}
return format(new Date(date), 'yyyy-MM-dd HH:mm:ss');
}
/**
* First Quality Assurance source loading after view initialization.
*/

View File

@@ -38,12 +38,12 @@
<tbody>
<tr *ngFor="let topicElement of (topics$ | async); let i = index">
<td>{{topicElement.name}}</td>
<td>{{formatDate(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 }}"
title="{{'quality-assurance.topics-list.button.detail' | translate : { param: topicElement.name } }}"
[routerLink]="[topicElement.id]">
<span class="badge badge-info">{{topicElement.totalEvents}}</span>
<i class="fas fa-info fa-fw"></i>
@@ -58,4 +58,12 @@
</ds-pagination>
</div>
</div>
<div class="row text-right">
<div class="col-md-12">
<a class="btn btn-outline-secondary" [routerLink]="['/notifications/quality-assurance']">
<i class="fas fa-angle-double-left"></i>
{{'quality-assurance.events.back-to-sources' | translate}}
</a>
</div>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { AfterViewInit, Component, OnDestroy, OnInit } from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import { distinctUntilChanged, map, take, tap } from 'rxjs/operators';
@@ -20,7 +20,6 @@ import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../../../core
import { Item } from '../../../core/shared/item.model';
import { getItemPageRoute } from '../../../item-page/item-page-routing-paths';
import { getNotificatioQualityAssuranceRoute } from '../../../admin/admin-routing-paths';
import { format } from 'date-fns';
/**
* Component to display the Quality Assurance topic list.
@@ -30,7 +29,7 @@ import { format } from 'date-fns';
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}
@@ -138,7 +137,7 @@ export class QualityAssuranceTopicsComponent implements OnInit {
* Dispatch the Quality Assurance topics retrival.
*/
public getQualityAssuranceTopics(source: string, target?: string): void {
this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig).pipe(
this.subs.push(this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig).pipe(
distinctUntilChanged(),
).subscribe((options: PaginationComponentOptions) => {
this.notificationsStateService.dispatchRetrieveQualityAssuranceTopics(
@@ -147,7 +146,7 @@ export class QualityAssuranceTopicsComponent implements OnInit {
source,
target
);
});
}));
}
/**
@@ -202,20 +201,6 @@ export class QualityAssuranceTopicsComponent implements OnInit {
return getNotificatioQualityAssuranceRoute();
}
/**
* Formats the given date string into the format 'yyyy-MM-dd HH:mm:ss'.
* If the date is falsy, an empty string is returned.
*
* @param date - The date string to format.
* @returns The formatted date string.
*/
formatDate(date: string): string {
if (!date) {
return '';
}
return format(new Date(date), 'yyyy-MM-dd HH:mm:ss');
}
/**
* Unsubscribe from all subscriptions.
*/

View File

@@ -9,13 +9,29 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/
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(
@@ -23,22 +39,36 @@ export class ItemWithdrawnReinstateModalComponent implements ModalBeforeDismiss
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,9 @@ export const REQUEST_REINSTATE = 'REQUEST/REINSTATE';
@Injectable({
providedIn: 'root'
})
/**
* Service for managing the withdrawn/reinstate modal for a DSO.
*/
export class DsoWithdrawnReinstateModalService {
constructor(

View File

@@ -518,7 +518,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.",
@@ -2480,9 +2480,9 @@
"item.page.version.create": "Create new version",
"item.page.withdrawn": "Withdraw this Item",
"item.page.withdrawn": "Request a withdrawal for this item",
"item.page.reinstate": "Reinstate 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",
@@ -3190,7 +3190,9 @@
"quality-assurance.table.actions": "Actions",
"quality-assurance.button.detail": "Show details",
"quality-assurance.source-list.button.detail": "Show topics for {{param}}",
"quality-assurance.topics-list.button.detail": "Show suggestions for {{param}}",
"quality-assurance.noTopics": "No topics found.",
@@ -3256,6 +3258,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",