Merged in coar-CST-12709 (pull request #1051)

Coar CST-12709

Approved-by: Stefano Maffei
This commit is contained in:
Alisa Ismailati
2023-11-23 09:01:23 +00:00
committed by Stefano Maffei
17 changed files with 488 additions and 5 deletions

View File

@@ -195,6 +195,9 @@ import {
CoarNotifyConfigDataService CoarNotifyConfigDataService
} from '../submission/sections/section-coar-notify/coar-notify-config-data.service'; } from '../submission/sections/section-coar-notify/coar-notify-config-data.service';
import { SubmissionCoarNotifyConfig } from '../submission/sections/section-coar-notify/submission-coar-notify.config'; import { SubmissionCoarNotifyConfig } from '../submission/sections/section-coar-notify/submission-coar-notify.config';
import { NotifyRequestsStatus } from '../item-page/simple/notify-requests-status/notify-requests-status.model';
import { NotifyRequestsStatusDataService } from './data/notify-services-status-data.service';
/** /**
* When not in production, endpoint responses can be mocked for testing purposes * When not in production, endpoint responses can be mocked for testing purposes
@@ -320,7 +323,8 @@ const PROVIDERS = [
SupervisionOrderDataService, SupervisionOrderDataService,
LdnServicesService, LdnServicesService,
LdnItemfiltersService, LdnItemfiltersService,
CoarNotifyConfigDataService CoarNotifyConfigDataService,
NotifyRequestsStatusDataService
]; ];
/** /**
@@ -404,8 +408,8 @@ export const models =
SuggestionSource, SuggestionSource,
LdnService, LdnService,
Itemfilter, Itemfilter,
SubmissionCoarNotifyConfig SubmissionCoarNotifyConfig,
NotifyRequestsStatus,
]; ];
@NgModule({ @NgModule({

View File

@@ -0,0 +1,46 @@
import { Injectable } from '@angular/core';
import { RequestService } from './request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { IdentifiableDataService } from './base/identifiable-data.service';
import { dataService } from './base/data-service.decorator';
import { NotifyRequestsStatus } from '../../item-page/simple/notify-requests-status/notify-requests-status.model';
import { NOTIFYREQUEST} from '../../item-page/simple/notify-requests-status/notify-requests-status.resource-type';
import { Observable, map, take, tap } from 'rxjs';
import { RemoteData } from './remote-data';
import { GetRequest } from './request.models';
@Injectable()
@dataService(NOTIFYREQUEST)
export class NotifyRequestsStatusDataService extends IdentifiableDataService<NotifyRequestsStatus> {
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected rdb: RemoteDataBuildService,
) {
super('notifyrequests', requestService, rdbService, objectCache, halService);
}
/**
* Retrieves the status of notify requests for a specific item.
* @param itemUuid The UUID of the item.
* @returns An Observable that emits the remote data containing the notify requests status.
*/
getNotifyRequestsStatus(itemUuid: string): Observable<RemoteData<NotifyRequestsStatus>> {
const href$ = this.getEndpoint().pipe(
map((url: string) => url + '/' + itemUuid ),
);
href$.pipe(take(1)).subscribe((url: string) => {
const request = new GetRequest(this.requestService.generateRequestId(), url);
this.requestService.send(request, true);
});
return this.rdb.buildFromHref(href$);
}
}

View File

@@ -61,6 +61,8 @@ import {
ThemedFullFileSectionComponent ThemedFullFileSectionComponent
} from './full/field-components/file-section/themed-full-file-section.component'; } from './full/field-components/file-section/themed-full-file-section.component';
import { QaEventNotificationComponent } from './simple/qa-event-notification/qa-event-notification.component'; import { QaEventNotificationComponent } from './simple/qa-event-notification/qa-event-notification.component';
import { NotifyRequestsStatusComponent } from './simple/notify-requests-status/notify-requests-status-component/notify-requests-status.component';
import { RequestStatusAlertBoxComponent } from './simple/notify-requests-status/request-status-alert-box/request-status-alert-box.component';
const ENTRY_COMPONENTS = [ const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator // put only entry components that use custom decorator
@@ -104,7 +106,9 @@ const DECLARATIONS = [
ItemAlertsComponent, ItemAlertsComponent,
ThemedItemAlertsComponent, ThemedItemAlertsComponent,
BitstreamRequestACopyPageComponent, BitstreamRequestACopyPageComponent,
QaEventNotificationComponent QaEventNotificationComponent,
NotifyRequestsStatusComponent,
RequestStatusAlertBoxComponent
]; ];
@NgModule({ @NgModule({

View File

@@ -3,6 +3,7 @@
<div *ngIf="itemRD?.payload as item"> <div *ngIf="itemRD?.payload as item">
<ds-themed-item-alerts [item]="item"></ds-themed-item-alerts> <ds-themed-item-alerts [item]="item"></ds-themed-item-alerts>
<ds-qa-event-notification [item]="item"></ds-qa-event-notification> <ds-qa-event-notification [item]="item"></ds-qa-event-notification>
<ds-notify-requests-status [itemUuid]="item.uuid"></ds-notify-requests-status>
<ds-item-versions-notice [item]="item"></ds-item-versions-notice> <ds-item-versions-notice [item]="item"></ds-item-versions-notice>
<ds-view-tracker [object]="item"></ds-view-tracker> <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> <ds-listable-object-component-loader *ngIf="!item.isWithdrawn || (isAdmin$|async)" [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>

View File

@@ -0,0 +1,5 @@
<ng-container *ngIf="(requestMap$ | async)?.size > 0">
<ng-container *ngFor="let entry of (requestMap$ | async) | keyvalue ">
<ds-request-status-alert-box [status]="entry.key" [data]="entry.value"></ds-request-status-alert-box>
</ng-container>
</ng-container>

View File

@@ -0,0 +1,88 @@
import { ComponentFixture, TestBed, tick, fakeAsync } from '@angular/core/testing';
import { NotifyRequestsStatusComponent } from './notify-requests-status.component';
import { NotifyRequestsStatusDataService } from 'src/app/core/data/notify-services-status-data.service';
import { NotifyRequestsStatus } from '../notify-requests-status.model';
import { RequestStatusEnum } from '../notify-status.enum';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
import { TranslateModule } from '@ngx-translate/core';
describe('NotifyRequestsStatusComponent', () => {
let component: NotifyRequestsStatusComponent;
let fixture: ComponentFixture<NotifyRequestsStatusComponent>;
let notifyInfoServiceSpy;
const mock: NotifyRequestsStatus = Object.assign(new NotifyRequestsStatus(), {
notifyStatus: [],
itemuuid: 'testUuid'
});
beforeEach(() => {
notifyInfoServiceSpy = {
getNotifyRequestsStatus:() => createSuccessfulRemoteDataObject$(mock)
};
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [NotifyRequestsStatusComponent],
providers: [
{ provide: NotifyRequestsStatusDataService, useValue: notifyInfoServiceSpy }
]
});
});
beforeEach(() => {
fixture = TestBed.createComponent(NotifyRequestsStatusComponent);
component = fixture.componentInstance;
});
it('should create the component', () => {
expect(component).toBeTruthy();
});
it('should fetch data from the service on initialization', fakeAsync(() => {
const mockData: NotifyRequestsStatus = Object.assign(new NotifyRequestsStatus(), {
notifyStatus: [],
itemuuid: 'testUuid'
});
component.itemUuid = mockData.itemuuid;
spyOn(notifyInfoServiceSpy, 'getNotifyRequestsStatus').and.callThrough();
component.ngOnInit();
fixture.detectChanges();
tick();
expect(notifyInfoServiceSpy.getNotifyRequestsStatus).toHaveBeenCalledWith('testUuid');
component.requestMap$.subscribe((map) => {
expect(map.size).toBe(0);
});
}));
it('should group data by status', () => {
const mockData: NotifyRequestsStatus = Object.assign(new NotifyRequestsStatus(), {
notifyStatus: [
{
serviceName: 'test1',
serviceUrl: 'test',
status: RequestStatusEnum.ACCEPTED,
},
{
serviceName: 'test2',
serviceUrl: 'test',
status: RequestStatusEnum.REJECTED,
},
{
serviceName: 'test3',
serviceUrl: 'test',
status: RequestStatusEnum.ACCEPTED,
},
],
itemUuid: 'testUuid'
});
spyOn(notifyInfoServiceSpy, 'getNotifyRequestsStatus').and.returnValue(createSuccessfulRemoteDataObject$(mockData));
fixture.detectChanges();
(component as any).groupDataByStatus(mockData);
component.requestMap$.subscribe((map) => {
expect(map.size).toBe(2);
expect(map.get(RequestStatusEnum.ACCEPTED)?.length).toBe(2);
expect(map.get(RequestStatusEnum.REJECTED)?.length).toBe(1);
});
});
});

View File

@@ -0,0 +1,73 @@
import {
ChangeDetectionStrategy,
Component,
Input,
OnInit,
} from '@angular/core';
import { Observable, filter, map } from 'rxjs';
import {
NotifyRequestsStatus,
NotifyStatuses,
} from '../notify-requests-status.model';
import { NotifyRequestsStatusDataService } from '../../../../core/data/notify-services-status-data.service';
import { RequestStatusEnum } from '../notify-status.enum';
import {
getFirstCompletedRemoteData,
getRemoteDataPayload,
} from '../../../../core/shared/operators';
import { hasValue } from '../../../../shared/empty.util';
@Component({
selector: 'ds-notify-requests-status',
templateUrl: './notify-requests-status.component.html',
styleUrls: ['./notify-requests-status.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NotifyRequestsStatusComponent implements OnInit {
/**
* The UUID of the item.
*/
@Input() itemUuid: string;
/**
* Observable representing the request map.
* The map contains request status enums as keys and arrays of notify statuses as values.
*/
requestMap$: Observable<Map<RequestStatusEnum, NotifyStatuses[]>>;
constructor(private notifyInfoService: NotifyRequestsStatusDataService) { }
ngOnInit(): void {
this.requestMap$ = this.notifyInfoService
.getNotifyRequestsStatus(this.itemUuid)
.pipe(
getFirstCompletedRemoteData(),
filter((data) => hasValue(data)),
getRemoteDataPayload(),
filter((data: NotifyRequestsStatus) => hasValue(data)),
map((data: NotifyRequestsStatus) => {
return this.groupDataByStatus(data);
})
);
}
/**
* Groups the notify requests status data by status.
* @param notifyRequestsStatus The notify requests status data.
*/
private groupDataByStatus(notifyRequestsStatus: NotifyRequestsStatus) {
const statusMap: Map<RequestStatusEnum, NotifyStatuses[]> = new Map();
notifyRequestsStatus.notifyStatus.forEach(
(notifyStatus: NotifyStatuses) => {
const status = notifyStatus.status;
if (!statusMap.has(status)) {
statusMap.set(status, []);
}
statusMap.get(status)?.push(notifyStatus);
}
);
return statusMap;
}
}

View File

@@ -0,0 +1,68 @@
// eslint-disable-next-line max-classes-per-file
import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
import { typedObject } from '../../../core/cache/builders/build-decorators';
import { CacheableObject } from '../../../core/cache/cacheable-object.model';
import { ResourceType } from '../../../core/shared/resource-type';
import { excludeFromEquals } from '../../../core/utilities/equals.decorators';
import { NOTIFYREQUEST } from './notify-requests-status.resource-type';
import { HALLink } from '../../../core/shared/hal-link.model';
import { RequestStatusEnum } from './notify-status.enum';
/**
* Represents the status of notify requests for an item.
*/
@typedObject
@inheritSerialization(CacheableObject)
export class NotifyRequestsStatus implements CacheableObject {
static type = NOTIFYREQUEST;
/**
* The object type.
*/
@excludeFromEquals
@autoserialize
type: ResourceType;
/**
* The notify statuses.
*/
@autoserialize
notifyStatus: NotifyStatuses[];
/**
* The UUID of the item.
*/
@autoserialize
itemuuid: string;
/**
* The links associated with the notify requests status.
*/
@deserialize
_links: {
self: HALLink;
[k: string]: HALLink | HALLink[];
};
}
/**
* Represents the status of a notification request.
*/
export class NotifyStatuses {
/**
* The name of the service.
*/
serviceName: string;
/**
* The URL of the service.
*/
serviceUrl: string;
/**
* The status of the notification request.
*/
status: RequestStatusEnum;
}

View File

@@ -0,0 +1,8 @@
import {ResourceType} from '../../../core/shared/resource-type';
/**
* The resource type for the root endpoint
*
* Needs to be in a separate file to prevent circular
* dependencies in webpack.
*/
export const NOTIFYREQUEST = new ResourceType('notifyrequests');

View File

@@ -0,0 +1,5 @@
export enum RequestStatusEnum {
ACCEPTED = 'ACCEPTED',
REJECTED = 'REJECTED',
REQUESTED = 'REQUESTED',
}

View File

@@ -0,0 +1,32 @@
<ng-container *ngIf="data?.length > 0 && displayOptions">
<div
[ngClass]="{'align-items-center': data.length == 1}"
class="alert d-flex flex-row sections-gap {{
displayOptions.alertType
}}"
>
<img
class="source-logo"
src="assets/images/qa-coar-notify-logo.png"
alt="notify logo"
/>
<ds-truncatable [id]="status">
<ds-truncatable-part [id]="status" [minLines]="1">
<div class="w-100 d-flex flex-column">
<ng-container *ngFor="let request of data">
<div
[innerHTML]="
displayOptions.text
| translate
: {
serviceName: request.serviceName,
serviceUrl: request.serviceUrl
}
"
></div>
</ng-container>
</div>
</ds-truncatable-part>
</ds-truncatable>
</div>
</ng-container>

View File

@@ -0,0 +1,7 @@
.source-logo {
max-height: var(--ds-header-logo-height);
}
.sections-gap {
gap: 1rem;
}

View File

@@ -0,0 +1,51 @@
import { ComponentFixture, TestBed, fakeAsync } from '@angular/core/testing';
import { RequestStatusAlertBoxComponent } from './request-status-alert-box.component';
import { TranslateModule } from '@ngx-translate/core';
import { RequestStatusEnum } from '../notify-status.enum';
describe('RequestStatusAlertBoxComponent', () => {
let component: RequestStatusAlertBoxComponent;
let componentAsAny: any;
let fixture: ComponentFixture<RequestStatusAlertBoxComponent>;
const mockData = [
{
serviceName: 'test',
serviceUrl: 'test',
status: RequestStatusEnum.ACCEPTED,
},
{
serviceName: 'test1',
serviceUrl: 'test',
status: RequestStatusEnum.REJECTED,
},
];
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [RequestStatusAlertBoxComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(RequestStatusAlertBoxComponent);
component = fixture.componentInstance;
component.data = mockData;
component.displayOptions = {
alertType: 'alert-danger',
text: 'request-status-alert-box.rejected',
};
componentAsAny = component;
fixture.detectChanges();
});
it('should create the component', () => {
expect(component).toBeTruthy();
});
it('should display the alert box when data is available', fakeAsync(() => {
const alertBoxElement = fixture.nativeElement.querySelector('.alert');
expect(alertBoxElement).toBeTruthy();
}));
});

View File

@@ -0,0 +1,82 @@
import {
ChangeDetectionStrategy,
Component,
Input,
type OnInit,
} from '@angular/core';
import { NotifyStatuses } from '../notify-requests-status.model';
import { RequestStatusEnum } from '../notify-status.enum';
@Component({
selector: 'ds-request-status-alert-box',
templateUrl: './request-status-alert-box.component.html',
styleUrls: ['./request-status-alert-box.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
/**
* Represents a component that displays the status of a request.
*/
export class RequestStatusAlertBoxComponent implements OnInit {
/**
* The status of the request.
*/
@Input() status: RequestStatusEnum;
/**
* The input data for the request status alert box component.
* @type {NotifyStatuses[]}
*/
@Input() data: NotifyStatuses[] = [];
/**
* The display options for the request status alert box.
*/
displayOptions: NotifyRequestDisplayOptions;
ngOnInit(): void {
this.prepareDataToDisplay();
}
/**
* Prepares the data to be displayed based on the current status.
*/
private prepareDataToDisplay() {
switch (this.status) {
case RequestStatusEnum.ACCEPTED:
this.displayOptions = {
alertType: 'alert-info',
text: 'request-status-alert-box.accepted',
};
break;
case RequestStatusEnum.REJECTED:
this.displayOptions = {
alertType: 'alert-danger',
text: 'request-status-alert-box.rejected',
};
break;
case RequestStatusEnum.REQUESTED:
this.displayOptions = {
alertType: 'alert-warning',
text: 'request-status-alert-box.requested',
};
break;
}
}
}
/**
* Represents the display options for a notification request.
*/
export interface NotifyRequestDisplayOptions {
/**
* The type of alert to display.
* Possible values are 'alert-danger', 'alert-warning', or 'alert-info'.
*/
alertType: 'alert-danger' | 'alert-warning' | 'alert-info';
/**
* The text to display in the notification.
*/
text: string;
}

View File

@@ -1,12 +1,14 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { getFirstCompletedRemoteData, getPaginatedListPayload, getRemoteDataPayload } from '../../../core/shared/operators'; import { getFirstCompletedRemoteData, getPaginatedListPayload, getRemoteDataPayload } from '../../../core/shared/operators';
import { Observable } from 'rxjs'; import { Observable, filter } from 'rxjs';
import { AlertType } from '../../../shared/alert/aletr-type'; import { AlertType } from '../../../shared/alert/aletr-type';
import { FindListOptions } from '../../../core/data/find-list-options.model'; import { FindListOptions } from '../../../core/data/find-list-options.model';
import { RequestParam } from '../../../core/cache/models/request-param.model'; import { RequestParam } from '../../../core/cache/models/request-param.model';
import { QualityAssuranceSourceDataService } from '../../../core/suggestion-notifications/qa/source/quality-assurance-source-data.service'; import { QualityAssuranceSourceDataService } from '../../../core/suggestion-notifications/qa/source/quality-assurance-source-data.service';
import { QualityAssuranceSourceObject } from '../../../core/suggestion-notifications/qa/models/quality-assurance-source.model'; import { QualityAssuranceSourceObject } from '../../../core/suggestion-notifications/qa/models/quality-assurance-source.model';
import { PaginatedList } from 'src/app/core/data/paginated-list.model';
import { hasValue } from 'src/app/shared/empty.util';
@Component({ @Component({
selector: 'ds-qa-event-notification', selector: 'ds-qa-event-notification',
@@ -47,6 +49,7 @@ export class QaEventNotificationComponent {
.pipe( .pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
getRemoteDataPayload(), getRemoteDataPayload(),
filter((pl: PaginatedList<QualityAssuranceSourceObject>) => hasValue(pl)),
getPaginatedListPayload(), getPaginatedListPayload(),
); );
} }

View File

@@ -5705,4 +5705,10 @@
"access-control-option-end-date-note": "Select the date until which the related access condition is applied", "access-control-option-end-date-note": "Select the date until which the related access condition is applied",
"request-status-alert-box.accepted": "The request for <a href='{{serviceUrl}}' target='_blank'> {{ serviceName }} </a> has been taken in charge.",
"request-status-alert-box.rejected": "The request for <a href='{{serviceUrl}}' target='_blank'> {{ serviceName }} </a> has been rejected.",
"request-status-alert-box.requested": "The request for <a href='{{serviceUrl}}' target='_blank'> {{ serviceName }} </a> is pending.",
} }