[CST-11178][CST-11179] page to select the target item for "relation" correction suggestion

This commit is contained in:
Mykhaylo
2023-10-30 12:21:38 +01:00
parent 752cb4d4b0
commit bf67fee89b
16 changed files with 576 additions and 20 deletions

View File

@@ -238,6 +238,11 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
canActivate: [AuthenticatedGuard] canActivate: [AuthenticatedGuard]
}, },
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent }, { path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent },
{
path: 'items',
loadChildren: () => import('./shared/correction-suggestion/correction-suggestion.module')
.then((m) => m.CorrectionSuggestionModule)
},
] ]
} }
], { ], {

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { find, take } from 'rxjs/operators'; import { find, switchMap, take } from 'rxjs/operators';
import { ReplaceOperation } from 'fast-json-patch'; import { ReplaceOperation } from 'fast-json-patch';
import { HALEndpointService } from '../../../shared/hal-endpoint.service'; import { HALEndpointService } from '../../../shared/hal-endpoint.service';
@@ -200,4 +200,25 @@ export class QualityAssuranceEventDataService extends IdentifiableDataService<Qu
return this.rdbService.buildFromRequestUUID<QualityAssuranceEventObject>(requestId); 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(data: string): Observable<RemoteData<OpenaireBrokerEventObject>> {
const requestId = this.requestService.generateRequestId();
const href$ = this.getBrowseEndpoint();
return href$.pipe(
switchMap((href: string) => {
const request = new PostRequest(requestId, href, data);
if (hasValue(this.responseMsToLive)) {
request.responseMsToLive = this.responseMsToLive;
}
this.requestService.send(request);
return this.rdbService.buildFromRequestUUID<OpenaireBrokerEventObject>(requestId);
})
);
}
} }

View File

@@ -4,8 +4,7 @@ import { DSpaceObject } from './../../../core/shared/dspace-object.model';
import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { ContextMenuEntryComponent } from '../context-menu-entry.component'; import { ContextMenuEntryComponent } from '../context-menu-entry.component';
import { ContextMenuEntryType } from '../context-menu-entry-type'; import { ContextMenuEntryType } from '../context-menu-entry-type';
import { BehaviorSubject, Observable, Subscription, map, startWith, tap } from 'rxjs'; import { BehaviorSubject, Observable, Subscription, map, startWith} from 'rxjs';
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { CorrectionTypeMode } from '../../../core/submission/models/correction-type-mode.model'; import { CorrectionTypeMode } from '../../../core/submission/models/correction-type-mode.model';
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
import { NotificationsService } from '../../notifications/notifications.service'; import { NotificationsService } from '../../notifications/notifications.service';
@@ -25,17 +24,6 @@ export class CorrectionTypeMenuComponent extends ContextMenuEntryComponent imple
*/ */
public static menuEntryType: ContextMenuEntryType = ContextMenuEntryType.CorrectionType; public static menuEntryType: ContextMenuEntryType = ContextMenuEntryType.CorrectionType;
/**
* A boolean representing if a request operation is pending
* @type {BehaviorSubject<boolean>}
*/
public processing$ = new BehaviorSubject<boolean>(false);
/**
* Reference to NgbModal
*/
public modalRef: NgbModalRef;
/** /**
* List of Edit Modes available on this item * List of Edit Modes available on this item
* for the current user * for the current user
@@ -51,9 +39,9 @@ export class CorrectionTypeMenuComponent extends ContextMenuEntryComponent imple
@Inject('contextMenuObjectProvider') protected injectedContextMenuObject: DSpaceObject, @Inject('contextMenuObjectProvider') protected injectedContextMenuObject: DSpaceObject,
@Inject('contextMenuObjectTypeProvider') protected injectedContextMenuObjectType: DSpaceObjectType, @Inject('contextMenuObjectTypeProvider') protected injectedContextMenuObjectType: DSpaceObjectType,
private correctionTypeService: CorrectionTypeDataService, private correctionTypeService: CorrectionTypeDataService,
public notificationService: NotificationsService public notificationService: NotificationsService,
) { ) {
super(injectedContextMenuObject, injectedContextMenuObjectType, ContextMenuEntryType.EditSubmission); super(injectedContextMenuObject, injectedContextMenuObjectType, ContextMenuEntryType.CorrectionType);
} }
ngOnInit(): void { ngOnInit(): void {
@@ -78,19 +66,26 @@ export class CorrectionTypeMenuComponent extends ContextMenuEntryComponent imple
); );
} }
/**
* Get correction types
* useCachedVersionIfAvailable = false to force refreshing the list
*/
getData(): void { getData(): void {
this.sub = this.correctionTypeService.findByItem(this.contextMenuObject.id, true).pipe( this.sub = this.correctionTypeService.findByItem(this.contextMenuObject.id, false).pipe(
tap((types) => console.log(types)),
getAllSucceededRemoteDataPayload(), getAllSucceededRemoteDataPayload(),
getPaginatedListPayload(), getPaginatedListPayload(),
startWith([]) startWith([])
).subscribe((types: CorrectionTypeMode[]) => { ).subscribe((types: CorrectionTypeMode[]) => {
console.log(types);
this.correctionTypes$.next(types); this.correctionTypes$.next(types);
}); });
} }
getTypeRoute(id: string) { /**
* Get the route to the correction type page
* @param id correction type id
* @returns the route to the correction type page
*/
getTypeRoute(id: string): string {
return getCorrectionTypePageRoute(this.contextMenuObject.id, id); return getCorrectionTypePageRoute(this.contextMenuObject.id, id);
} }

View File

@@ -0,0 +1,15 @@
const pageMap = new Map();
export function renderCorrectionFor(creationMode: string) {
return function decorator(component: any) {
if (!component) {
return;
}
pageMap.set(creationMode, component);
};
}
export function getCorrectionComponent(creationMode: string) {
return pageMap.get(creationMode);
}

View File

@@ -0,0 +1,23 @@
import { ItemBreadcrumbResolver } from './../../core/breadcrumbs/item-breadcrumb.resolver';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CorrectionSuggestionComponent } from './correction-suggestion.component';
const routes: Routes = [
{
path: ':uuid/corrections/:correctionType',
component: CorrectionSuggestionComponent,
resolve: {
breadcrumb: ItemBreadcrumbResolver,
},
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: [
ItemBreadcrumbResolver
]
})
export class CorrectionSuggestionRoutingModule { }

View File

@@ -0,0 +1,3 @@
<div class="container">
<ng-container *ngComponentOutlet="getComponent(); injector: objectInjector"></ng-container>
</div>

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CorrectionSuggestionComponent } from './correction-suggestion.component';
describe('CorrectionSuggestionComponent', () => {
let component: CorrectionSuggestionComponent;
let fixture: ComponentFixture<CorrectionSuggestionComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ CorrectionSuggestionComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(CorrectionSuggestionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,94 @@
import { getRemoteDataPayload } from './../../core/shared/operators';
import { CorrectionTypeDataService } from './../../core/submission/correctiontype-data.service';
import { Component, ComponentFactoryResolver, Injector, OnInit } from '@angular/core';
import { CorrectionTypeMode } from '../../core/submission/models/correction-type-mode.model';
import { GenericConstructor } from '../../core/shared/generic-constructor';
import { getCorrectionComponent } from './correction-suggestion-page.decorator';
import { ActivatedRoute, Params } from '@angular/router';
import { hasValue } from '../empty.util';
import { getFirstCompletedRemoteData } from 'src/app/core/shared/operators';
@Component({
selector: 'ds-correction-suggestion',
templateUrl: './correction-suggestion.component.html',
styleUrls: ['./correction-suggestion.component.scss']
})
export class CorrectionSuggestionComponent implements OnInit {
/**
* The correction type object
*/
public correctionTypeObject: CorrectionTypeMode;
/**
* The correction type id
*/
private correctionTypeId: string;
/**
* The creation form
*/
private creationForm: string;
/**
* The injector for the component
*/
public objectInjector: Injector;
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
private aroute: ActivatedRoute,
private correctionTypeDataService: CorrectionTypeDataService,
private injector: Injector,
) {
this.aroute.params.subscribe((params: Params) => {
this.correctionTypeId = params.correctionType;
});
}
ngOnInit(): void {
this.initComponent();
}
/**
* Initialize the component by fetching the correction type object
* and rendering the correct component based on the creation form
*/
initComponent(): void {
if (hasValue(this.correctionTypeId)) {
this.correctionTypeDataService.getCorrectionTypeById(this.correctionTypeId)
.pipe(
getFirstCompletedRemoteData(),
getRemoteDataPayload(),
)
.subscribe((correctionType: CorrectionTypeMode) => {
if (hasValue(correctionType)) {
this.correctionTypeObject = correctionType;
this.creationForm = correctionType.creationForm;
this.componentFactoryResolver.resolveComponentFactory(this.getComponent());
this.injectData();
}
});
}
}
/**
* Inject the data into the component
*/
private injectData(): void {
this.objectInjector = Injector.create({
providers: [
{ provide: 'correctionTypeObjectProvider', useValue: this.correctionTypeObject, deps: [] },
],
parent: this.injector,
});
}
/**
* Fetch the component depending on the creation form
* @returns {GenericConstructor<Component>}
*/
getComponent(): GenericConstructor<Component> {
return getCorrectionComponent(this.creationForm);
}
}

View File

@@ -0,0 +1,39 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CorrectionSuggestionComponent } from './correction-suggestion.component';
import { SharedModule } from '../shared.module';
import { CorrectionSuggestionRoutingModule } from './correction-suggestion-routing.module';
import { ManageRelationCorrectionTypeComponent } from './correction-types/manage-relation-correction-type/manage-relation-correction-type.component';
import { SearchModule } from '../search/search.module';
const COMPONENTS = [
CorrectionSuggestionComponent,
ManageRelationCorrectionTypeComponent,
];
const ENTRY_COMPONENTS = [
ManageRelationCorrectionTypeComponent,
];
@NgModule({
declarations: [
COMPONENTS,
],
imports: [
CommonModule,
CorrectionSuggestionRoutingModule,
SharedModule,
SearchModule
],
exports: [
COMPONENTS,
]
})
export class CorrectionSuggestionModule {
static withEntryComponents() {
return {
ngModule: CorrectionSuggestionModule,
providers: ENTRY_COMPONENTS.map((component) => ({ provide: component }))
};
}
}

View File

@@ -0,0 +1,3 @@
export enum CorrectionTypeForms {
MANAGE_RELATION = 'manageRelation',
}

View File

@@ -0,0 +1,54 @@
<div class="row justify-content-center">
<h4>
{{ ( 'correction-type.manage-relation.' + (correctionType.topic | lowercase) + '.searchHeader' | translate) }}
</h4>
</div>
<div class="pt-3">
<div id="project-entities" class="mb-3">
<div id="project-search" class="input-group mb-3">
<input type="text" class="form-control" (keyup.enter)="search(projectTitle)" [(ngModel)]="projectTitle"
[placeholder]="'correction-type.manage-relation.search.placeholder' | translate" aria-label="" aria-describedby="">
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary" [disabled]="projectTitle === ''"
(click)="projectTitle = ''">{{('correction-type.manage-relation.search.btn.clear'|translate)}}</button>
<button type="button" class="btn btn-primary" [disabled]="projectTitle === ''"
(click)="search(projectTitle)">{{('correction-type.manage-relation.search.btn.search'|translate)}}</button>
</div>
</div>
<ds-loading *ngIf="(isLoading$ | async)" message="{{'loading.search-results' | translate}}"></ds-loading>
<ds-search-results *ngIf="(localEntitiesRD$ | async)?.payload?.page?.length > 0 && !(isLoading$ | async)"
[searchResults]="(localEntitiesRD$ | async)"
[sortConfig]="this.searchOptions?.sort"
[searchConfig]="this.searchOptions"
[selectable]="true"
[disableHeader]="true"
[hidePaginationDetail]="false"
[selectionConfig]="{ repeatable: false, listId: entityListId }"
[linkType]="linkTypes.ExternalLink"
[context]="context"
(deselectObject)="deselectEntity()"
(selectObject)="selectEntity($event)">
</ds-search-results>
<div *ngIf="(localEntitiesRD$ | async)?.payload?.page?.length < 1 && !(isLoading$ | async)">
<ds-alert [type]="'alert-info'">
<p class="lead mb-0">{{( 'correction-type.manage-relation.search.notFound' | translate)}}</p>
</ds-alert>
</div>
</div>
<div class="flex-row d-flex justify-content-between">
<div>
<button type="button" class="btn btn-outline-secondary" (click)="back()">
{{ ( 'correction-type.manage-relation.' + (correctionType.topic | lowercase) + '.cancel' | translate) }}
</button>
</div>
<div>
<button type="button" class="btn btn-primary" [disabled]="selectedImportType === importType.None" (click)="performAction()">
{{ ( 'correction-type.manage-relation.' + (correctionType.topic | lowercase) + '.submit' | translate) }}
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ManageRelationCorrectionTypeComponent } from './manage-relation-correction-type.component';
describe('ManageRelationCorrectionTypeComponent', () => {
let component: ManageRelationCorrectionTypeComponent;
let fixture: ComponentFixture<ManageRelationCorrectionTypeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ManageRelationCorrectionTypeComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ManageRelationCorrectionTypeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,252 @@
import { NotificationsService } from './../../../notifications/notifications.service';
import { OpenaireBrokerEventObject } from './../../../../core/openaire/broker/models/openaire-broker-event.model';
import { getFirstSucceededRemoteDataPayload } from './../../../../core/shared/operators';
import { ItemDataService } from './../../../../core/data/item-data.service';
import { OpenaireBrokerEventRestService } from './../../../../core/openaire/broker/events/openaire-broker-event-rest.service';
import { Context } from './../../../../core/shared/context.model';
import { CollectionElementLinkType } from './../../../object-collection/collection-element-link.type';
import { DSpaceObject } from './../../../../core/shared/dspace-object.model';
import { SearchResult } from './../../../search/models/search-result.model';
import { RemoteData } from './../../../../core/data/remote-data';
import { PaginatedList } from './../../../../core/data/paginated-list.model';
import { PaginatedSearchOptions } from './../../../search/models/paginated-search-options.model';
import { SelectableListService } from './../../../object-list/selectable-list/selectable-list.service';
import { SearchService } from './../../../../core/shared/search/search.service';
import { PaginationComponentOptions } from './../../../pagination/pagination-component-options.model';
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { renderCorrectionFor } from '../../correction-suggestion-page.decorator';
import { CorrectionTypeMode } from '../../../../core/submission/models/correction-type-mode.model';
import { CorrectionTypeForms } from './../correction-type-forms';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { Observable, Subscription, of as observableOf, switchMap } from 'rxjs';
import { hasValue, isNotEmpty } from '../../../../shared/empty.util';
import { ListableObject } from '../../../../shared/object-collection/shared/listable-object.model';
import { Item } from '../../../../core/shared/item.model';
import { ImportType } from '../../../../openaire/broker/project-entry-import-modal/project-entry-import-modal.component';
@Component({
selector: 'ds-manage-relation-correction-type',
templateUrl: './manage-relation-correction-type.component.html',
styleUrls: ['./manage-relation-correction-type.component.scss']
})
@renderCorrectionFor(CorrectionTypeForms.MANAGE_RELATION)
export class ManageRelationCorrectionTypeComponent implements OnInit, OnDestroy {
/**
* The correction type object
*/
correctionType: CorrectionTypeMode;
/**
* The item uuid from the parent object
*/
itemUuid: string;
/**
* The project title from the parent object
*/
projectTitle = '';
/**
* Pagination options
*/
pagination: PaginationComponentOptions;
/**
* The number of results per page
*/
pageSize = 3;
/**
* Entities to show in the list
*/
localEntitiesRD$: Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>;
/**
* Search options to use for fetching projects
*/
searchOptions: PaginatedSearchOptions;
/**
* The list of subscriptions
*/
protected subs: Subscription[] = [];
/**
* Information about the data loading status
*/
isLoading$ = observableOf(true);
/**
* The selected local entity
*/
selectedEntity: ListableObject;
/**
* The type of link to render in listable elements
*/
linkTypes = CollectionElementLinkType;
/**
* The context we're currently in (submission)
*/
context = Context.Search;
/**
* List ID for selecting local entities
*/
entityListId = 'correction-suggestion-manage-relation';
/**
* List ID for selecting local authorities
*/
authorityListId = 'correction-suggestion-manage-relation-authority';
/**
* ImportType enum
*/
importType = ImportType;
/**
* The type of import the user currently has selected
*/
selectedImportType = ImportType.None;
constructor(
@Inject('correctionTypeObjectProvider') private correctionTypeObject: CorrectionTypeMode,
public searchService: SearchService,
private selectService: SelectableListService,
private aroute: ActivatedRoute,
private openaireBrokerEventRestService: OpenaireBrokerEventRestService,
private itemService: ItemDataService,
private notificationsService: NotificationsService,
private router: Router
) {
this.correctionType = correctionTypeObject;
this.aroute.params.subscribe((params: Params) => {
this.itemUuid = params.uuid;
});
}
/**
* Get the search results
*/
ngOnInit(): void {
this.pagination = Object.assign(new PaginationComponentOptions(), { id: 'correction-suggestion-manage-relation', pageSize: this.pageSize });
this.searchOptions = Object.assign(new PaginatedSearchOptions(
{
configuration: this.correctionType.discoveryConfiguration,
scope: this.itemUuid,
pagination: this.pagination
}
));
this.localEntitiesRD$ = this.searchService.search(this.searchOptions);
this.subs.push(
this.localEntitiesRD$.subscribe(
() => this.isLoading$ = observableOf(false)
)
);
}
/**
* Perform a project search by title.
*/
public search(searchTitle): void {
if (isNotEmpty(searchTitle)) {
const filterRegEx = /[:]/g;
this.isLoading$ = observableOf(true);
this.searchOptions = Object.assign(new PaginatedSearchOptions(
{
configuration: this.correctionType.discoveryConfiguration,
query: (searchTitle) ? searchTitle.replace(filterRegEx, '') : searchTitle,
scope: this.itemUuid,
pagination: this.pagination
}
));
this.localEntitiesRD$ = this.searchService.search(this.searchOptions);
this.subs.push(
this.localEntitiesRD$.subscribe(
() => this.isLoading$ = observableOf(false)
)
);
}
}
/**
* Deselected a local entity
*/
public deselectEntity(): void {
this.selectedEntity = undefined;
if (this.selectedImportType === ImportType.LocalEntity) {
this.selectedImportType = ImportType.None;
}
}
/**
* Selected a local entity
* @param entity
*/
public selectEntity(entity): void {
this.selectedEntity = entity;
this.selectedImportType = ImportType.LocalEntity;
}
/**
* Deselect every element from both entity and authority lists
*/
public deselectAllLists(): void {
this.selectService.deselectAll(this.entityListId);
this.selectService.deselectAll(this.authorityListId);
}
/**
* Perform the action based on the correction type
* by posting the data to the OpenAIRE Broker Event API.
* Data is formatted as follows, in the exact order:
* <item link>
* <selected entity link>
* <correction type link>
*/
performAction() {
if (hasValue(this.selectedEntity)) {
const selectedItemLink = (this.selectedEntity as SearchResult<DSpaceObject>).indexableObject._links.self.href;
this.itemService.findById(this.itemUuid).pipe(
getFirstSucceededRemoteDataPayload(),
switchMap((item: Item) => {
console.log(item);
const data: string = item._links.self.href + '\n' + selectedItemLink + '\n' + this.correctionTypeObject._links.self.href;
return this.openaireBrokerEventRestService.postData(data);
})
).subscribe((res: RemoteData<OpenaireBrokerEventObject>) => {
if (res.hasSucceeded) {
this.selectedImportType = ImportType.None;
// TODO: show success message based on the type of correction
this.notificationsService.success('Correction suggestion submitted', 'The correction suggestion has been submitted');
this.deselectAllLists();
this.back();
} else {
this.notificationsService.error('Error submitting correction suggestion', 'The correction suggestion could not be submitted');
}
});
}
}
/**
* Navigate back to the previous page
*/
back() {
this.router.navigate(['../'], { relativeTo: this.aroute });
}
/**
* Unsubscribe from all subscriptions.
*/
ngOnDestroy(): void {
this.deselectAllLists();
this.subs
.filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe());
}
}

View File

@@ -1,3 +1,4 @@
import { CorrectionSuggestionModule } from './../../app/shared/correction-suggestion/correction-suggestion.module';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { AdminRegistriesModule } from '../../app/admin/admin-registries/admin-registries.module'; import { AdminRegistriesModule } from '../../app/admin/admin-registries/admin-registries.module';
@@ -299,6 +300,7 @@ const DECLARATIONS = [
NgxGalleryModule, NgxGalleryModule,
FormModule, FormModule,
RequestCopyModule, RequestCopyModule,
CorrectionSuggestionModule,
], ],
declarations: DECLARATIONS, declarations: DECLARATIONS,
exports: [ exports: [