[TLC-674] Duplicate detection comp, template, i18n

Duplicate data is accessed in the submission section,
pooled tasks list and claimed tasks list.
This commit is contained in:
Kim Shepherd
2023-12-19 14:47:27 +13:00
parent 061129ecb7
commit b672668e15
13 changed files with 266 additions and 4 deletions

View File

@@ -242,6 +242,26 @@ export abstract class BaseItemDataService extends IdentifiableDataService<Item>
); );
} }
public getDuplicatesEndpoint(itemId: string): Observable<string> {
return this.halService.getEndpoint(this.linkPath).pipe(
switchMap((url: string) => this.halService.getEndpoint('duplicates', `${url}/${itemId}`))
);
}
public getDuplicates(itemId: string, searchOptions?: PaginatedSearchOptions): Observable<RemoteData<PaginatedList<Item>>> {
const hrefObs = this.getDuplicatesEndpoint(itemId).pipe(
map((href) => searchOptions ? searchOptions.toRestUrl(href) : href)
);
hrefObs.pipe(
take(1)
).subscribe((href) => {
const request = new GetRequest(this.requestService.generateRequestId(), href);
this.requestService.send(request);
});
return this.rdbService.buildList<Item>(hrefObs);
}
/** /**
* Get the endpoint to move the item * Get the endpoint to move the item
* @param itemId * @param itemId

View File

@@ -26,6 +26,7 @@ import { AccessStatusObject } from 'src/app/shared/object-collection/shared/badg
import { HandleObject } from './handle-object.model'; import { HandleObject } from './handle-object.model';
import { IDENTIFIERS } from '../../shared/object-list/identifier-data/identifier-data.resource-type'; import { IDENTIFIERS } from '../../shared/object-list/identifier-data/identifier-data.resource-type';
import { IdentifierData } from '../../shared/object-list/identifier-data/identifier-data.model'; import { IdentifierData } from '../../shared/object-list/identifier-data/identifier-data.model';
import {Duplicate} from "../../shared/object-list/duplicate-data/duplicate.model";
/** /**
* Class representing a DSpace Item * Class representing a DSpace Item
@@ -79,6 +80,7 @@ export class Item extends DSpaceObject implements ChildHALResource, HandleObject
thumbnail: HALLink; thumbnail: HALLink;
accessStatus: HALLink; accessStatus: HALLink;
identifiers: HALLink; identifiers: HALLink;
duplicates: HALLink;
self: HALLink; self: HALLink;
}; };
@@ -131,6 +133,9 @@ export class Item extends DSpaceObject implements ChildHALResource, HandleObject
@link(IDENTIFIERS, false, 'identifiers') @link(IDENTIFIERS, false, 'identifiers')
identifiers?: Observable<RemoteData<IdentifierData>>; identifiers?: Observable<RemoteData<IdentifierData>>;
@link(ITEM, true, 'duplicates')
duplicates?: Observable<RemoteData<PaginatedList<Duplicate>>>
/** /**
* Method that returns as which type of object this object should be rendered * Method that returns as which type of object this object should be rendered
*/ */

View File

@@ -0,0 +1,8 @@
/*
* Object model for the data returned by the REST API to present minted identifiers in a submission section
*/
import { Duplicate } from '../../../shared/object-list/duplicate-data/duplicate.model';
export interface WorkspaceitemSectionDuplicatesObject {
potentialDuplicates?: Duplicate[]
}

View File

@@ -0,0 +1,24 @@
import {autoserialize} from "cerialize";
import {MetadataMap} from "../../../core/shared/metadata.models";
export class Duplicate {
/**
* The item title
*/
@autoserialize
title: string;
@autoserialize
uuid: string;
@autoserialize
workflowItemId: bigint;
@autoserialize
workspaceItemId: bigint;
@autoserialize
owningCollection: string;
/**
* Metadata for the bitstream (e.g. dc.description)
*/
@autoserialize
metadata: MetadataMap;
}

View File

@@ -0,0 +1,9 @@
import { ResourceType } from 'src/app/core/shared/resource-type';
/**
* The resource type for Access Status
*
* Needs to be in a separate file to prevent circular
* dependencies in webpack.
*/
export const DUPLICATE = new ResourceType('duplicate');

View File

@@ -21,6 +21,7 @@ import { Item } from '../../../../core/shared/item.model';
import { mergeMap, tap } from 'rxjs/operators'; import { mergeMap, tap } from 'rxjs/operators';
import { isNotEmpty, hasValue } from '../../../empty.util'; import { isNotEmpty, hasValue } from '../../../empty.util';
import { Context } from '../../../../core/shared/context.model'; import { Context } from '../../../../core/shared/context.model';
import {Duplicate} from "../../duplicate-data/duplicate.model";
@Component({ @Component({
selector: 'ds-claimed-search-result-list-element', selector: 'ds-claimed-search-result-list-element',
@@ -50,6 +51,11 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle
*/ */
public workflowitem$: BehaviorSubject<WorkflowItem> = new BehaviorSubject<WorkflowItem>(null); public workflowitem$: BehaviorSubject<WorkflowItem> = new BehaviorSubject<WorkflowItem>(null);
/**
* The potential duplicates of this item
*/
public duplicates$: Observable<Duplicate[]>;
/** /**
* Display thumbnails if required by configuration * Display thumbnails if required by configuration
*/ */

View File

@@ -4,6 +4,14 @@
[showSubmitter]="showSubmitter" [showSubmitter]="showSubmitter"
[badgeContext]="badgeContext" [badgeContext]="badgeContext"
[workflowItem]="workflowitem$.value"></ds-themed-item-list-preview> [workflowItem]="workflowitem$.value"></ds-themed-item-list-preview>
<ng-container *ngVar="(duplicates$|async).length as duplicateCount">
<div [ngClass]="'col-md-12'" *ngIf="duplicateCount > 0">
<div class="d-flex">
{{ duplicateCount }} {{ 'submission.workflow.tasks.duplicates' | translate }}
</div>
</div>
</ng-container>
<div class="row"> <div class="row">
<div [ngClass]="showThumbnails ? 'offset-3 offset-md-2 pl-3' : ''"> <div [ngClass]="showThumbnails ? 'offset-3 offset-md-2 pl-3' : ''">
<ds-pool-task-actions id="actions" <ds-pool-task-actions id="actions"

View File

@@ -1,7 +1,7 @@
import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { BehaviorSubject, EMPTY, Observable } from 'rxjs'; import { BehaviorSubject, EMPTY, Observable } from 'rxjs';
import { mergeMap, tap } from 'rxjs/operators'; import {map, mergeMap, tap} from 'rxjs/operators';
import { ViewMode } from '../../../../core/shared/view-mode.model'; import { ViewMode } from '../../../../core/shared/view-mode.model';
import { RemoteData } from '../../../../core/data/remote-data'; import { RemoteData } from '../../../../core/data/remote-data';
@@ -22,6 +22,8 @@ import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { isNotEmpty, hasValue } from '../../../empty.util'; import { isNotEmpty, hasValue } from '../../../empty.util';
import { Context } from '../../../../core/shared/context.model'; import { Context } from '../../../../core/shared/context.model';
import {PaginatedList} from "../../../../core/data/paginated-list.model";
import {Duplicate} from "../../duplicate-data/duplicate.model";
/** /**
* This component renders pool task object for the search result in the list view. * This component renders pool task object for the search result in the list view.
@@ -55,6 +57,11 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen
*/ */
public workflowitem$: BehaviorSubject<WorkflowItem> = new BehaviorSubject<WorkflowItem>(null); public workflowitem$: BehaviorSubject<WorkflowItem> = new BehaviorSubject<WorkflowItem>(null);
/**
* The potential duplicates of this workflow item
*/
public duplicates$: Observable<Duplicate[]>;
/** /**
* The index of this list element * The index of this list element
*/ */
@@ -81,7 +88,7 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen
ngOnInit() { ngOnInit() {
super.ngOnInit(); super.ngOnInit();
this.linkService.resolveLinks(this.dso, followLink('workflowitem', {}, this.linkService.resolveLinks(this.dso, followLink('workflowitem', {},
followLink('item', {}, followLink('bundles')), followLink('item', {}, followLink('bundles'), followLink('duplicates')),
followLink('submitter') followLink('submitter')
), followLink('action')); ), followLink('action'));
@@ -100,6 +107,19 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen
tap((itemRD: RemoteData<Item>) => { tap((itemRD: RemoteData<Item>) => {
if (isNotEmpty(itemRD) && itemRD.hasSucceeded) { if (isNotEmpty(itemRD) && itemRD.hasSucceeded) {
this.item$.next(itemRD.payload); this.item$.next(itemRD.payload);
console.dir(itemRD.payload);
this.duplicates$ = itemRD.payload.duplicates.pipe(
getFirstCompletedRemoteData(),
map((remoteData: RemoteData<PaginatedList<Duplicate>>) => {
console.dir(remoteData);
if (remoteData.hasSucceeded) {
if (remoteData.payload.page) {
console.dir(remoteData.payload.page);
return remoteData.payload.page;
}
}
})
);
} }
}) })
).subscribe(); ).subscribe();

View File

@@ -0,0 +1,20 @@
<!--
Template for the detect duplicates submission section component
@author Kim Shepherd
-->
<div class="text-sm-left" *ngVar="(this.data$ | async) as data">
<ng-container *ngIf="data.potentialDuplicates.length == 0">
<p>{{ 'submission.sections.duplicates.none' }}</p>
</ng-container>
<ng-container *ngIf="data.potentialDuplicates.length > 0">
<p>{{ 'submission.sections.duplicates.detected' | translate }}</p>
<div *ngFor="let dupe of data.potentialDuplicates" class="ds-duplicate">
<a target="_blank" [href]="'/items/'+dupe.uuid">{{dupe.title}}</a>
<div *ngFor="let metadatum of Metadata.toViewModelList(dupe.metadata)">
{{('item.preview.' + metadatum.key) | translate}} {{metadatum.value}}
</div>
<p *ngIf="dupe.workspaceItemId">{{ 'submission.sections.duplicates.in-workspace' | translate }}</p>
<p *ngIf="dupe.workflowItemId">{{ 'submission.sections.duplicates.in-workflow' | translate }}</p>
</div>
</ng-container>
</div>

View File

@@ -0,0 +1,127 @@
import {ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { Observable, of as observableOf, Subscription } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { SectionsType } from '../sections-type';
import { SectionModelComponent } from '../models/section.model';
import { renderSectionFor } from '../sections-decorator';
import { SectionDataObject } from '../models/section-data.model';
import { SubmissionService } from '../../submission.service';
import { AlertType } from '../../../shared/alert/alert-type';
import { SectionsService } from '../sections.service';
import {map} from "rxjs/operators";
import {ItemDataService} from "../../../core/data/item-data.service";
import {
WorkspaceitemSectionDuplicatesObject
} from "../../../core/submission/models/workspaceitem-section-duplicates.model";
import {Metadata} from "../../../core/shared/metadata.utils";
/**
* Detect duplicates step
*
* @author Kim Shepherd
*/
@Component({
selector: 'ds-submission-section-duplicates',
templateUrl: './section-duplicates.component.html',
changeDetection: ChangeDetectionStrategy.Default
})
@renderSectionFor(SectionsType.Duplicates)
export class SubmissionSectionDuplicatesComponent extends SectionModelComponent {
/**
* The Alert categories.
* @type {AlertType}
*/
public AlertTypeEnum = AlertType;
/**
* Variable to track if the section is loading.
* @type {boolean}
*/
public isLoading = true;
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
protected subs: Subscription[] = [];
/**
* Section data observable
*/
public data$: Observable<WorkspaceitemSectionDuplicatesObject>;
/**
* Initialize instance variables.
*
* @param {TranslateService} translate
* @param {SectionsService} sectionService
* @param {SubmissionService} submissionService
* @param itemDataService
* @param nameService
* @param {string} injectedCollectionId
* @param {SectionDataObject} injectedSectionData
* @param {string} injectedSubmissionId
*/
constructor(protected translate: TranslateService,
protected sectionService: SectionsService,
protected submissionService: SubmissionService,
private itemDataService: ItemDataService,
// private nameService: DSONameService,
@Inject('collectionIdProvider') public injectedCollectionId: string,
@Inject('sectionDataProvider') public injectedSectionData: SectionDataObject,
@Inject('submissionIdProvider') public injectedSubmissionId: string) {
super(injectedCollectionId, injectedSectionData, injectedSubmissionId);
}
ngOnInit() {
super.ngOnInit();
}
/**
* Initialize all instance variables and retrieve configuration.
*/
onSectionInit() {
this.isLoading = false;
this.data$ = this.getDuplicateData().pipe(
map((data: WorkspaceitemSectionDuplicatesObject) => {
console.dir(data);
return data;
})
);
}
/**
* Check if identifier section has read-only visibility
*/
isReadOnly(): boolean {
return true;
}
/**
* Unsubscribe from all subscriptions, if needed.
*/
onSectionDestroy(): void {
return;
}
/**
* Get section status. Because this simple component never requires human interaction, this is basically
* always going to be the opposite of "is this section still loading". This is not the place for API response
* error checking but determining whether the step can 'proceed'.
*
* @return Observable<boolean>
* the section status
*/
public getSectionStatus(): Observable<boolean> {
return observableOf(!this.isLoading);
}
public getDuplicateData(): Observable<WorkspaceitemSectionDuplicatesObject> {
return this.sectionService.getSectionData(this.submissionId, this.sectionData.id, this.sectionData.sectionType) as
Observable<WorkspaceitemSectionDuplicatesObject>;
}
protected readonly Metadata = Metadata;
}

View File

@@ -10,4 +10,5 @@ export enum SectionsType {
Identifiers = 'identifiers', Identifiers = 'identifiers',
Collection = 'collection', Collection = 'collection',
CoarNotify = 'coarnotify' CoarNotify = 'coarnotify'
Duplicates = 'duplicates'
} }

View File

@@ -72,6 +72,7 @@ import {
CoarNotifyConfigDataService CoarNotifyConfigDataService
} from './sections/section-coar-notify/coar-notify-config-data.service'; } from './sections/section-coar-notify/coar-notify-config-data.service';
import { LdnServicesService } from '../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service'; import { LdnServicesService } from '../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service';
import { SubmissionSectionDuplicatesComponent } from './sections/duplicates/section-duplicates.component';
const ENTRY_COMPONENTS = [ const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator // put only entry components that use custom decorator
@@ -81,7 +82,8 @@ const ENTRY_COMPONENTS = [
SubmissionSectionCcLicensesComponent, SubmissionSectionCcLicensesComponent,
SubmissionSectionAccessesComponent, SubmissionSectionAccessesComponent,
SubmissionSectionSherpaPoliciesComponent, SubmissionSectionSherpaPoliciesComponent,
SubmissionSectionCoarNotifyComponent SubmissionSectionCoarNotifyComponent,
SubmissionSectionDuplicatesComponent
]; ];
const DECLARATIONS = [ const DECLARATIONS = [

View File

@@ -2829,6 +2829,8 @@
"item.preview.organization.legalName": "Legal Name", "item.preview.organization.legalName": "Legal Name",
"item.preview.dspace.entity.type": "Entity Type:",
"item.select.confirm": "Confirm selected", "item.select.confirm": "Confirm selected",
"item.select.empty": "No items to show", "item.select.empty": "No items to show",
@@ -5111,7 +5113,7 @@
"submission.sections.submit.progressbar.describe.steptwo": "Describe", "submission.sections.submit.progressbar.describe.steptwo": "Describe",
"submission.sections.submit.progressbar.detect-duplicate": "Potential duplicates", "submission.sections.submit.progressbar.duplicates": "Potential duplicates",
"submission.sections.submit.progressbar.identifiers": "Identifiers", "submission.sections.submit.progressbar.identifiers": "Identifiers",
@@ -5243,6 +5245,14 @@
"submission.sections.accesses.form.until-placeholder": "Until", "submission.sections.accesses.form.until-placeholder": "Until",
"submission.sections.duplicates.none": "No duplicates were detected.",
"submission.sections.duplicates.detected": "Potential duplicates were detected. Please review the list below.",
"submission.sections.duplicates.in-workspace": "This item is in workspace",
"submission.sections.duplicates.in-workflow": "This item is in workflow",
"submission.sections.license.granted-label": "I confirm the license above", "submission.sections.license.granted-label": "I confirm the license above",
"submission.sections.license.required": "You must accept the license", "submission.sections.license.required": "You must accept the license",
@@ -5369,6 +5379,8 @@
"submission.workflow.tasks.pool.show-detail": "Show detail", "submission.workflow.tasks.pool.show-detail": "Show detail",
"submission.workflow.tasks.duplicates": "potential duplicates were detected for this item. Claim and edit this item to see details.",
"submission.workspace.generic.view": "View", "submission.workspace.generic.view": "View",
"submission.workspace.generic.view-help": "Select this option to view the item's metadata.", "submission.workspace.generic.view-help": "Select this option to view the item's metadata.",