From ab4657610a15457cdd9d9836b6657f77341f82be Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 19 Dec 2023 14:47:27 +1300 Subject: [PATCH] [TLC-674] Duplicate detection comp, template, i18n Duplicate data is accessed in the submission section, pooled tasks list and claimed tasks list. --- src/app/core/data/item-data.service.ts | 20 +++ src/app/core/shared/item.model.ts | 5 + .../workspaceitem-section-duplicates.model.ts | 8 ++ .../duplicate-data/duplicate.model.ts | 24 ++++ .../duplicate-data/duplicate.resource-type.ts | 9 ++ ...ed-search-result-list-element.component.ts | 6 + ...-search-result-list-element.component.html | 8 ++ ...ol-search-result-list-element.component.ts | 24 +++- .../section-duplicates.component.html | 20 +++ .../section-duplicates.component.ts | 127 ++++++++++++++++++ src/app/submission/sections/sections-type.ts | 1 + src/app/submission/submission.module.ts | 32 +++-- src/assets/i18n/en.json5 | 14 +- 13 files changed, 281 insertions(+), 17 deletions(-) create mode 100644 src/app/core/submission/models/workspaceitem-section-duplicates.model.ts create mode 100644 src/app/shared/object-list/duplicate-data/duplicate.model.ts create mode 100644 src/app/shared/object-list/duplicate-data/duplicate.resource-type.ts create mode 100644 src/app/submission/sections/duplicates/section-duplicates.component.html create mode 100644 src/app/submission/sections/duplicates/section-duplicates.component.ts diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index c3fa84dd6c8..f2af44401a7 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -242,6 +242,26 @@ export abstract class BaseItemDataService extends IdentifiableDataService ); } + public getDuplicatesEndpoint(itemId: string): Observable { + return this.halService.getEndpoint(this.linkPath).pipe( + switchMap((url: string) => this.halService.getEndpoint('duplicates', `${url}/${itemId}`)) + ); + } + + public getDuplicates(itemId: string, searchOptions?: PaginatedSearchOptions): Observable>> { + 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(hrefObs); + } + /** * Get the endpoint to move the item * @param itemId diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index 20fc275ee28..6d8346eb12b 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -26,6 +26,7 @@ import { AccessStatusObject } from 'src/app/shared/object-collection/shared/badg import { HandleObject } from './handle-object.model'; import { IDENTIFIERS } from '../../shared/object-list/identifier-data/identifier-data.resource-type'; 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 @@ -79,6 +80,7 @@ export class Item extends DSpaceObject implements ChildHALResource, HandleObject thumbnail: HALLink; accessStatus: HALLink; identifiers: HALLink; + duplicates: HALLink; self: HALLink; }; @@ -131,6 +133,9 @@ export class Item extends DSpaceObject implements ChildHALResource, HandleObject @link(IDENTIFIERS, false, 'identifiers') identifiers?: Observable>; + @link(ITEM, true, 'duplicates') + duplicates?: Observable>> + /** * Method that returns as which type of object this object should be rendered */ diff --git a/src/app/core/submission/models/workspaceitem-section-duplicates.model.ts b/src/app/core/submission/models/workspaceitem-section-duplicates.model.ts new file mode 100644 index 00000000000..b31c2d1b349 --- /dev/null +++ b/src/app/core/submission/models/workspaceitem-section-duplicates.model.ts @@ -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[] +} diff --git a/src/app/shared/object-list/duplicate-data/duplicate.model.ts b/src/app/shared/object-list/duplicate-data/duplicate.model.ts new file mode 100644 index 00000000000..7ca0364a01d --- /dev/null +++ b/src/app/shared/object-list/duplicate-data/duplicate.model.ts @@ -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; +} diff --git a/src/app/shared/object-list/duplicate-data/duplicate.resource-type.ts b/src/app/shared/object-list/duplicate-data/duplicate.resource-type.ts new file mode 100644 index 00000000000..bbaba8f79d4 --- /dev/null +++ b/src/app/shared/object-list/duplicate-data/duplicate.resource-type.ts @@ -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'); diff --git a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.ts index 18148b6a8c4..e3537e7310d 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.ts @@ -21,6 +21,7 @@ import { Item } from '../../../../core/shared/item.model'; import { mergeMap, tap } from 'rxjs/operators'; import { isNotEmpty, hasValue } from '../../../empty.util'; import { Context } from '../../../../core/shared/context.model'; +import {Duplicate} from "../../duplicate-data/duplicate.model"; @Component({ selector: 'ds-claimed-search-result-list-element', @@ -50,6 +51,11 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle */ public workflowitem$: BehaviorSubject = new BehaviorSubject(null); + /** + * The potential duplicates of this item + */ + public duplicates$: Observable; + /** * Display thumbnails if required by configuration */ diff --git a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.html b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.html index 9fe6e37c9e3..dbc115aac2c 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.html +++ b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.html @@ -4,6 +4,14 @@ [showSubmitter]="showSubmitter" [badgeContext]="badgeContext" [workflowItem]="workflowitem$.value"> + +
+
+ {{ duplicateCount }} {{ 'submission.workflow.tasks.duplicates' | translate }} +
+
+
+
= new BehaviorSubject(null); + /** + * The potential duplicates of this workflow item + */ + public duplicates$: Observable; + /** * The index of this list element */ @@ -81,7 +88,7 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen ngOnInit() { super.ngOnInit(); this.linkService.resolveLinks(this.dso, followLink('workflowitem', {}, - followLink('item', {}, followLink('bundles')), + followLink('item', {}, followLink('bundles'), followLink('duplicates')), followLink('submitter') ), followLink('action')); @@ -100,6 +107,19 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen tap((itemRD: RemoteData) => { if (isNotEmpty(itemRD) && itemRD.hasSucceeded) { this.item$.next(itemRD.payload); + console.dir(itemRD.payload); + this.duplicates$ = itemRD.payload.duplicates.pipe( + getFirstCompletedRemoteData(), + map((remoteData: RemoteData>) => { + console.dir(remoteData); + if (remoteData.hasSucceeded) { + if (remoteData.payload.page) { + console.dir(remoteData.payload.page); + return remoteData.payload.page; + } + } + }) + ); } }) ).subscribe(); diff --git a/src/app/submission/sections/duplicates/section-duplicates.component.html b/src/app/submission/sections/duplicates/section-duplicates.component.html new file mode 100644 index 00000000000..02805b12d51 --- /dev/null +++ b/src/app/submission/sections/duplicates/section-duplicates.component.html @@ -0,0 +1,20 @@ + +
+ +

{{ 'submission.sections.duplicates.none' }}

+
+ +

{{ 'submission.sections.duplicates.detected' | translate }}

+
+ {{dupe.title}} +
+ {{('item.preview.' + metadatum.key) | translate}} {{metadatum.value}} +
+

{{ 'submission.sections.duplicates.in-workspace' | translate }}

+

{{ 'submission.sections.duplicates.in-workflow' | translate }}

+
+
+
diff --git a/src/app/submission/sections/duplicates/section-duplicates.component.ts b/src/app/submission/sections/duplicates/section-duplicates.component.ts new file mode 100644 index 00000000000..54d3080fb00 --- /dev/null +++ b/src/app/submission/sections/duplicates/section-duplicates.component.ts @@ -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; + + /** + * 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 + * the section status + */ + public getSectionStatus(): Observable { + return observableOf(!this.isLoading); + } + + public getDuplicateData(): Observable { + return this.sectionService.getSectionData(this.submissionId, this.sectionData.id, this.sectionData.sectionType) as + Observable; + } + + protected readonly Metadata = Metadata; +} diff --git a/src/app/submission/sections/sections-type.ts b/src/app/submission/sections/sections-type.ts index 6bca8a72526..bfb170b1f7f 100644 --- a/src/app/submission/sections/sections-type.ts +++ b/src/app/submission/sections/sections-type.ts @@ -9,4 +9,5 @@ export enum SectionsType { SherpaPolicies = 'sherpaPolicy', Identifiers = 'identifiers', Collection = 'collection', + Duplicates = 'duplicates' } diff --git a/src/app/submission/submission.module.ts b/src/app/submission/submission.module.ts index cf0ab2b369a..d3c1f8db800 100644 --- a/src/app/submission/submission.module.ts +++ b/src/app/submission/submission.module.ts @@ -67,6 +67,8 @@ import { } from './sections/sherpa-policies/metadata-information/metadata-information.component'; import { SectionFormOperationsService } from './sections/form/section-form-operations.service'; import {SubmissionSectionIdentifiersComponent} from './sections/identifiers/section-identifiers.component'; +import {SubmissionSectionDuplicatesComponent} from "./sections/duplicates/section-duplicates.component"; +import {ItemSharedModule} from "../item-page/item-shared.module"; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -76,6 +78,7 @@ const ENTRY_COMPONENTS = [ SubmissionSectionCcLicensesComponent, SubmissionSectionAccessesComponent, SubmissionSectionSherpaPoliciesComponent, + SubmissionSectionDuplicatesComponent, ]; const DECLARATIONS = [ @@ -109,20 +112,21 @@ const DECLARATIONS = [ ]; @NgModule({ - imports: [ - CommonModule, - CoreModule.forRoot(), - SharedModule, - StoreModule.forFeature('submission', submissionReducers, storeModuleConfig as StoreConfig), - EffectsModule.forFeature(submissionEffects), - JournalEntitiesModule.withEntryComponents(), - ResearchEntitiesModule.withEntryComponents(), - FormModule, - NgbModalModule, - NgbCollapseModule, - NgbAccordionModule, - UploadModule, - ], + imports: [ + CommonModule, + CoreModule.forRoot(), + SharedModule, + StoreModule.forFeature('submission', submissionReducers, storeModuleConfig as StoreConfig), + EffectsModule.forFeature(submissionEffects), + JournalEntitiesModule.withEntryComponents(), + ResearchEntitiesModule.withEntryComponents(), + FormModule, + NgbModalModule, + NgbCollapseModule, + NgbAccordionModule, + UploadModule, + ItemSharedModule, + ], declarations: DECLARATIONS, exports: [ ...DECLARATIONS, diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 6dee5d54e66..f085e64b18b 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2502,6 +2502,8 @@ "item.preview.oaire.fundingStream": "Funding Stream:", + "item.preview.dspace.entity.type": "Entity Type:", + "item.select.confirm": "Confirm selected", "item.select.empty": "No items to show", @@ -4396,7 +4398,7 @@ "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", @@ -4524,6 +4526,14 @@ "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.required": "You must accept the license", @@ -4650,6 +4660,8 @@ "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-help": "Select this option to view the item's metadata.",