From b672668e15eb43cd154bdd9afa456ead833bc923 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 19 Dec 2023 14:47:27 +1300 Subject: [PATCH 01/23] [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 | 4 +- src/assets/i18n/en.json5 | 14 +- 13 files changed, 266 insertions(+), 4 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 5f71d1731d5..f06e3bc8fc8 100644 --- a/src/app/submission/sections/sections-type.ts +++ b/src/app/submission/sections/sections-type.ts @@ -10,4 +10,5 @@ export enum SectionsType { Identifiers = 'identifiers', Collection = 'collection', CoarNotify = 'coarnotify' + Duplicates = 'duplicates' } diff --git a/src/app/submission/submission.module.ts b/src/app/submission/submission.module.ts index d839565f8d6..8324aa70bb7 100644 --- a/src/app/submission/submission.module.ts +++ b/src/app/submission/submission.module.ts @@ -72,6 +72,7 @@ import { CoarNotifyConfigDataService } 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 { SubmissionSectionDuplicatesComponent } from './sections/duplicates/section-duplicates.component'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -81,7 +82,8 @@ const ENTRY_COMPONENTS = [ SubmissionSectionCcLicensesComponent, SubmissionSectionAccessesComponent, SubmissionSectionSherpaPoliciesComponent, - SubmissionSectionCoarNotifyComponent + SubmissionSectionCoarNotifyComponent, + SubmissionSectionDuplicatesComponent ]; const DECLARATIONS = [ diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 58adaf9663b..c24beb615ee 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2829,6 +2829,8 @@ "item.preview.organization.legalName": "Legal Name", + "item.preview.dspace.entity.type": "Entity Type:", + "item.select.confirm": "Confirm selected", "item.select.empty": "No items to show", @@ -5111,7 +5113,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", @@ -5243,6 +5245,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", @@ -5369,6 +5379,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.", From 0e4ad5e674f7ef7073f678faed121b4ce6ddf77a Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 23 Jan 2024 14:00:14 +1300 Subject: [PATCH 02/23] [TLC-674] Tidy up components, WIP spec tests --- .../workspaceitem-section-duplicates.model.ts | 2 +- .../models/workspaceitem-sections.model.ts | 2 + .../duplicate-data/duplicate.model.ts | 4 +- ...arch-result-list-element.component.spec.ts | 1 + ...ol-search-result-list-element.component.ts | 1 - .../section-duplicates.component.html | 4 +- .../section-duplicates.component.spec.ts | 252 ++++++++++++++++++ .../section-duplicates.component.ts | 25 +- 8 files changed, 267 insertions(+), 24 deletions(-) create mode 100644 src/app/submission/sections/duplicates/section-duplicates.component.spec.ts diff --git a/src/app/core/submission/models/workspaceitem-section-duplicates.model.ts b/src/app/core/submission/models/workspaceitem-section-duplicates.model.ts index b31c2d1b349..f9441fa7905 100644 --- a/src/app/core/submission/models/workspaceitem-section-duplicates.model.ts +++ b/src/app/core/submission/models/workspaceitem-section-duplicates.model.ts @@ -1,5 +1,5 @@ /* - * Object model for the data returned by the REST API to present minted identifiers in a submission section + * Object model for the data returned by the REST API to present potential duplicates in a submission section */ import { Duplicate } from '../../../shared/object-list/duplicate-data/duplicate.model'; diff --git a/src/app/core/submission/models/workspaceitem-sections.model.ts b/src/app/core/submission/models/workspaceitem-sections.model.ts index dd19c3fb8b4..9f39c1ab04b 100644 --- a/src/app/core/submission/models/workspaceitem-sections.model.ts +++ b/src/app/core/submission/models/workspaceitem-sections.model.ts @@ -5,6 +5,7 @@ import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload import { WorkspaceitemSectionCcLicenseObject } from './workspaceitem-section-cc-license.model'; import {WorkspaceitemSectionIdentifiersObject} from './workspaceitem-section-identifiers.model'; import { WorkspaceitemSectionSherpaPoliciesObject } from './workspaceitem-section-sherpa-policies.model'; +import {WorkspaceitemSectionDuplicatesObject} from "./workspaceitem-section-duplicates.model"; /** * An interface to represent submission's section object. @@ -25,6 +26,7 @@ export type WorkspaceitemSectionDataType | WorkspaceitemSectionAccessesObject | WorkspaceitemSectionSherpaPoliciesObject | WorkspaceitemSectionIdentifiersObject + | WorkspaceitemSectionDuplicatesObject | string; diff --git a/src/app/shared/object-list/duplicate-data/duplicate.model.ts b/src/app/shared/object-list/duplicate-data/duplicate.model.ts index 7ca0364a01d..858f284d5c9 100644 --- a/src/app/shared/object-list/duplicate-data/duplicate.model.ts +++ b/src/app/shared/object-list/duplicate-data/duplicate.model.ts @@ -10,9 +10,9 @@ export class Duplicate { @autoserialize uuid: string; @autoserialize - workflowItemId: bigint; + workflowItemId: number; @autoserialize - workspaceItemId: bigint; + workspaceItemId: number; @autoserialize owningCollection: string; diff --git a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts index d63ee4ba139..9aad4a8b7b9 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts @@ -35,6 +35,7 @@ const mockResultObject: PoolTaskSearchResult = new PoolTaskSearchResult(); mockResultObject.hitHighlights = {}; const item = Object.assign(new Item(), { + duplicates: observableOf([]), bundles: observableOf({}), metadata: { 'dc.title': [ diff --git a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts index c91948116cb..ea87ce1b2e2 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts @@ -114,7 +114,6 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen console.dir(remoteData); if (remoteData.hasSucceeded) { if (remoteData.payload.page) { - console.dir(remoteData.payload.page); return remoteData.payload.page; } } diff --git a/src/app/submission/sections/duplicates/section-duplicates.component.html b/src/app/submission/sections/duplicates/section-duplicates.component.html index 02805b12d51..285c6b2b68a 100644 --- a/src/app/submission/sections/duplicates/section-duplicates.component.html +++ b/src/app/submission/sections/duplicates/section-duplicates.component.html @@ -2,14 +2,14 @@ Template for the detect duplicates submission section component @author Kim Shepherd --> -
+

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

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

- {{dupe.title}} + {{dupe.title}}
{{('item.preview.' + metadatum.key) | translate}} {{metadatum.value}}
diff --git a/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts b/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts new file mode 100644 index 00000000000..d15a255f14d --- /dev/null +++ b/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts @@ -0,0 +1,252 @@ +import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +import { NgxPaginationModule } from 'ngx-pagination'; +import { cold } from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; +import { TranslateModule } from '@ngx-translate/core'; + +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { createTestComponent } from '../../../shared/testing/utils.test'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; +import { SubmissionService } from '../../submission.service'; +import { SubmissionServiceStub } from '../../../shared/testing/submission-service.stub'; +import { SectionsService } from '../sections.service'; +import { SectionsServiceStub } from '../../../shared/testing/sections-service.stub'; +import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; +import { getMockFormOperationsService } from '../../../shared/mocks/form-operations-service.mock'; +import { getMockFormService } from '../../../shared/mocks/form-service.mock'; +import { FormService } from '../../../shared/form/form.service'; +import { SubmissionFormsConfigDataService } from '../../../core/config/submission-forms-config-data.service'; +import { SectionsType } from '../sections-type'; +import { mockSubmissionCollectionId, mockSubmissionId } from '../../../shared/mocks/submission.mock'; +import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; +import { SubmissionSectionDuplicatesComponent } from './section-duplicates.component'; +import { CollectionDataService } from '../../../core/data/collection-data.service'; +import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; +import { SectionFormOperationsService } from '../form/section-form-operations.service'; +import { SubmissionScopeType } from '../../../core/submission/submission-scope-type'; +import { License } from '../../../core/shared/license.model'; +import { Collection } from '../../../core/shared/collection.model'; +import { ObjNgFor } from '../../../shared/utils/object-ngfor.pipe'; +import { VarDirective } from '../../../shared/utils/var.directive'; +import { PaginationService } from '../../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; +import {Duplicate} from "../../../shared/object-list/duplicate-data/duplicate.model"; +import {MetadataValue} from "../../../core/shared/metadata.models"; +import { + WorkspaceitemSectionDuplicatesObject +} from "../../../core/submission/models/workspaceitem-section-duplicates.model"; +import {SectionDataObject} from "../models/section-data.model"; +import {defaultUUID} from "../../../shared/mocks/uuid.service.mock"; + +function getMockSubmissionFormsConfigService(): SubmissionFormsConfigDataService { + return jasmine.createSpyObj('FormOperationsService', { + getConfigAll: jasmine.createSpy('getConfigAll'), + getConfigByHref: jasmine.createSpy('getConfigByHref'), + getConfigByName: jasmine.createSpy('getConfigByName'), + getConfigBySearch: jasmine.createSpy('getConfigBySearch') + }); +} + +function getMockCollectionDataService(): CollectionDataService { + return jasmine.createSpyObj('CollectionDataService', { + findById: jasmine.createSpy('findById'), + findByHref: jasmine.createSpy('findByHref') + }); +} + +const duplicates: Duplicate[]= [{ + title: 'Unique title', + uuid: defaultUUID, + workflowItemId: 1, + workspaceItemId: 2, + owningCollection: 'Test Collection', + metadata: { + 'dc.title': [ + Object.assign(new MetadataValue(), { + 'value': 'Unique title', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + })] + } + }]; + +const potentialDuplicates: WorkspaceitemSectionDuplicatesObject = { + potentialDuplicates: duplicates +}; + +const sectionObject: SectionDataObject = { + header: 'submission.sections.submit.progressbar.duplicates', + config: 'https://dspace.org/api/config/submissionforms/duplicates', + mandatory: true, + opened: true, + data: potentialDuplicates, + errorsToShow: [], + serverValidationErrors: [], + id: 'duplicates', + sectionType: SectionsType.Duplicates, + sectionVisibility: null +}; + +describe('SubmissionSectionDuplicatesComponent test suite', () => { + let comp: SubmissionSectionDuplicatesComponent; + let compAsAny: any; + let fixture: ComponentFixture; + let submissionServiceStub: any = new SubmissionServiceStub(); + const sectionsServiceStub: any = new SectionsServiceStub(); + let formService: any; + let formOperationsService: any; + let formBuilderService: any; + let collectionDataService: any; + + const submissionId = mockSubmissionId; + const collectionId = mockSubmissionCollectionId; + const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', { + add: jasmine.createSpy('add'), + replace: jasmine.createSpy('replace'), + remove: jasmine.createSpy('remove'), + }); + + const licenseText = 'License text'; + const mockCollection = Object.assign(new Collection(), { + name: 'Community 1-Collection 1', + id: collectionId, + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'Community 1-Collection 1' + }], + license: createSuccessfulRemoteDataObject$(Object.assign(new License(), { text: licenseText })) + }); + const paginationService = new PaginationServiceStub(); + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + BrowserModule, + CommonModule, + FormsModule, + ReactiveFormsModule, + NgxPaginationModule, + NoopAnimationsModule, + TranslateModule.forRoot(), + ], + declarations: [ + SubmissionSectionDuplicatesComponent, + TestComponent, + ObjNgFor, + VarDirective, + ], + providers: [ + { provide: CollectionDataService, useValue: getMockCollectionDataService() }, + { provide: SectionFormOperationsService, useValue: getMockFormOperationsService() }, + { provide: FormService, useValue: getMockFormService() }, + { provide: JsonPatchOperationsBuilder, useValue: jsonPatchOpBuilder }, + { provide: SubmissionFormsConfigDataService, useValue: getMockSubmissionFormsConfigService() }, + { provide: NotificationsService, useClass: NotificationsServiceStub }, + { provide: SectionsService, useClass: SectionsServiceStub }, + { provide: SubmissionService, useClass: SubmissionServiceStub }, + { provide: 'collectionIdProvider', useValue: collectionId }, + { provide: 'sectionDataProvider', useValue: sectionObject }, + { provide: 'submissionIdProvider', useValue: submissionId }, + { provide: PaginationService, useValue: paginationService }, + ChangeDetectorRef, + FormBuilderService, + SubmissionSectionDuplicatesComponent + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents().then(); + })); + + // First test to check the correct component creation + describe('', () => { + let testComp: TestComponent; + let testFixture: ComponentFixture; + + // synchronous beforeEach + beforeEach(() => { + sectionsServiceStub.isSectionReadOnly.and.returnValue(observableOf(false)); + sectionsServiceStub.getSectionErrors.and.returnValue(observableOf([])); + sectionsServiceStub.getSectionData.and.returnValue(observableOf(sectionObject)); + const html = ``; + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + }); + + afterEach(() => { + testFixture.destroy(); + }); + + it('should create SubmissionSectionDuplicatesComponent', () => { + expect(testComp).toBeDefined(); + }); + }); + + describe('', () => { + beforeEach(() => { + fixture = TestBed.createComponent(SubmissionSectionDuplicatesComponent); + comp = fixture.componentInstance; + compAsAny = comp; + submissionServiceStub = TestBed.inject(SubmissionService); + formService = TestBed.inject(FormService); + formBuilderService = TestBed.inject(FormBuilderService); + formOperationsService = TestBed.inject(SectionFormOperationsService); + collectionDataService = TestBed.inject(CollectionDataService); + compAsAny.pathCombiner = new JsonPatchOperationPathCombiner('sections', sectionObject.id); + }); + + afterEach(() => { + fixture.destroy(); + comp = null; + compAsAny = null; + }); + + // Test initialisation of the submission section + it('Should init section properly', () => { + collectionDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockCollection)); + sectionsServiceStub.getSectionErrors.and.returnValue(observableOf([])); + sectionsServiceStub.isSectionReadOnly.and.returnValue(observableOf(false)); + compAsAny.submissionService.getSubmissionScope.and.returnValue(SubmissionScopeType.WorkspaceItem); + spyOn(comp, 'getSectionStatus').and.returnValue(observableOf(true)); + spyOn(comp, 'getDuplicateData').and.returnValue(observableOf(potentialDuplicates)); + expect(comp.isLoading).toBeTruthy(); + comp.onSectionInit(); + fixture.detectChanges(); + expect(comp.isLoading).toBeFalsy(); + }); + + // The following tests look for proper logic in the getSectionStatus() implementation + // These are very simple as we don't really have a 'false' state unless we're still loading + it('Should return TRUE if the isLoading is FALSE', () => { + compAsAny.isLoading = false; + expect(compAsAny.getSectionStatus()).toBeObservable(cold('(a|)', { + a: true + })); + }); + it('Should return FALSE', () => { + compAsAny.isLoadin = true; + expect(compAsAny.getSectionStatus()).toBeObservable(cold('(a|)', { + a: false + })); + }); + }); + +}); + +// declare a test component +@Component({ + selector: 'ds-test-cmp', + template: `` +}) +class TestComponent { + +} diff --git a/src/app/submission/sections/duplicates/section-duplicates.component.ts b/src/app/submission/sections/duplicates/section-duplicates.component.ts index 54d3080fb00..a8a64a0d17d 100644 --- a/src/app/submission/sections/duplicates/section-duplicates.component.ts +++ b/src/app/submission/sections/duplicates/section-duplicates.component.ts @@ -9,12 +9,12 @@ 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"; +import {URLCombiner} from "../../../core/url-combiner/url-combiner"; +import {getItemModuleRoute} from "../../../item-page/item-page-routing-paths"; /** * Detect duplicates step @@ -47,19 +47,12 @@ export class SubmissionSectionDuplicatesComponent extends SectionModelComponent */ 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 @@ -67,8 +60,6 @@ export class SubmissionSectionDuplicatesComponent extends SectionModelComponent 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) { @@ -84,13 +75,7 @@ export class SubmissionSectionDuplicatesComponent extends SectionModelComponent */ 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 @@ -123,5 +108,9 @@ export class SubmissionSectionDuplicatesComponent extends SectionModelComponent Observable; } + public getItemLink(uuid: any) { + return new URLCombiner(getItemModuleRoute(), uuid).toString(); + } + protected readonly Metadata = Metadata; } From 43620a44f28d8b20462d7988b67b8bb23e0ccdfd Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 23 Jan 2024 14:11:45 +1300 Subject: [PATCH 03/23] [TLC-674] Lint fixes --- src/app/core/shared/item.model.ts | 4 ++-- .../object-list/duplicate-data/duplicate.model.ts | 4 ++-- .../claimed-search-result-list-element.component.html | 8 +++++++- .../claimed-search-result-list-element.component.ts | 2 +- .../pool-search-result-list-element.component.ts | 4 ++-- .../duplicates/section-duplicates.component.ts | 11 ++++++----- src/app/submission/submission.module.ts | 5 +++-- 7 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index 6d8346eb12b..5ef670c42b9 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -26,7 +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"; +import { Duplicate } from '../../shared/object-list/duplicate-data/duplicate.model'; /** * Class representing a DSpace Item @@ -134,7 +134,7 @@ export class Item extends DSpaceObject implements ChildHALResource, HandleObject identifiers?: Observable>; @link(ITEM, true, 'duplicates') - duplicates?: Observable>> + duplicates?: Observable>>; /** * Method that returns as which type of object this object should be rendered diff --git a/src/app/shared/object-list/duplicate-data/duplicate.model.ts b/src/app/shared/object-list/duplicate-data/duplicate.model.ts index 858f284d5c9..d40e01a05c4 100644 --- a/src/app/shared/object-list/duplicate-data/duplicate.model.ts +++ b/src/app/shared/object-list/duplicate-data/duplicate.model.ts @@ -1,5 +1,5 @@ -import {autoserialize} from "cerialize"; -import {MetadataMap} from "../../../core/shared/metadata.models"; +import { autoserialize } from 'cerialize'; +import { MetadataMap } from '../../../core/shared/metadata.models'; export class Duplicate { /** diff --git a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.html b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.html index 42af008cdd7..fd4cf8a238f 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.html +++ b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.html @@ -4,7 +4,13 @@ [showSubmitter]="showSubmitter" [badgeContext]="badgeContext" [workflowItem]="workflowitem$.value"> - + +
+
+ {{ duplicateCount }} {{ 'submission.workflow.tasks.duplicates' | translate }} +
+
+
Date: Tue, 23 Jan 2024 15:51:29 +1300 Subject: [PATCH 04/23] [TLC-674] Unit test fixes for comp, claimed/pooled tasks --- .../duplicate-data/duplicate.model.ts | 3 ++ ...-search-result-list-element.component.html | 3 +- ...ed-search-result-list-element.component.ts | 15 ++++++++-- ...-search-result-list-element.component.html | 3 +- ...ol-search-result-list-element.component.ts | 5 ++-- .../section-duplicates.component.spec.ts | 30 +++++++------------ 6 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/app/shared/object-list/duplicate-data/duplicate.model.ts b/src/app/shared/object-list/duplicate-data/duplicate.model.ts index d40e01a05c4..c5cd1705e1b 100644 --- a/src/app/shared/object-list/duplicate-data/duplicate.model.ts +++ b/src/app/shared/object-list/duplicate-data/duplicate.model.ts @@ -21,4 +21,7 @@ export class Duplicate { */ @autoserialize metadata: MetadataMap; + + @autoserialize + type: string; } diff --git a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.html b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.html index fd4cf8a238f..dd9fec79d30 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.html +++ b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.html @@ -4,7 +4,8 @@ [showSubmitter]="showSubmitter" [badgeContext]="badgeContext" [workflowItem]="workflowitem$.value"> - + +
{{ duplicateCount }} {{ 'submission.workflow.tasks.duplicates' | translate }} 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 7bee6d2997d..fd855193517 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 @@ -18,10 +18,11 @@ import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interfac import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; import { Item } from '../../../../core/shared/item.model'; -import { mergeMap, tap } from 'rxjs/operators'; +import {map, 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'; +import { PaginatedList } from "../../../../core/data/paginated-list.model"; @Component({ selector: 'ds-claimed-search-result-list-element', @@ -54,7 +55,7 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle /** * The potential duplicates of this item */ - public duplicates$: Observable; + public duplicates$: Observable = new Observable(); /** * Display thumbnails if required by configuration @@ -96,6 +97,16 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle tap((itemRD: RemoteData) => { if (isNotEmpty(itemRD) && itemRD.hasSucceeded) { this.item$.next(itemRD.payload); + this.duplicates$ = itemRD.payload.duplicates.pipe( + getFirstCompletedRemoteData(), + map((remoteData: RemoteData>) => { + if (remoteData.hasSucceeded) { + if (remoteData.payload.page) { + return remoteData.payload.page; + } + } + }) + ); } }) ).subscribe(); 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 dbc115aac2c..d80bcc9b4c2 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,7 +4,8 @@ [showSubmitter]="showSubmitter" [badgeContext]="badgeContext" [workflowItem]="workflowitem$.value"> - + +
{{ duplicateCount }} {{ 'submission.workflow.tasks.duplicates' | translate }} diff --git a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts index 9f3b6e2715c..8a93d52e9ff 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts @@ -60,7 +60,7 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen /** * The potential duplicates of this workflow item */ - public duplicates$: Observable; + public duplicates$: Observable = new Observable(); /** * The index of this list element @@ -107,11 +107,9 @@ 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) { return remoteData.payload.page; @@ -124,6 +122,7 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen ).subscribe(); this.showThumbnails = this.appConfig.browseBy.showThumbnails; + } ngOnDestroy() { diff --git a/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts b/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts index d15a255f14d..4ca1469ebe2 100644 --- a/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts +++ b/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts @@ -2,7 +2,7 @@ import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { CommonModule } from '@angular/common'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NgxPaginationModule } from 'ngx-pagination'; @@ -11,7 +11,6 @@ import { of as observableOf } from 'rxjs'; import { TranslateModule } from '@ngx-translate/core'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -import { createTestComponent } from '../../../shared/testing/utils.test'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { SubmissionService } from '../../submission.service'; @@ -39,10 +38,6 @@ import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; import {Duplicate} from "../../../shared/object-list/duplicate-data/duplicate.model"; import {MetadataValue} from "../../../core/shared/metadata.models"; -import { - WorkspaceitemSectionDuplicatesObject -} from "../../../core/submission/models/workspaceitem-section-duplicates.model"; -import {SectionDataObject} from "../models/section-data.model"; import {defaultUUID} from "../../../shared/mocks/uuid.service.mock"; function getMockSubmissionFormsConfigService(): SubmissionFormsConfigDataService { @@ -76,19 +71,15 @@ const duplicates: Duplicate[]= [{ 'confidence': -1, 'place': 0 })] - } + }, + type: "DUPLICATE" }]; -const potentialDuplicates: WorkspaceitemSectionDuplicatesObject = { - potentialDuplicates: duplicates -}; - -const sectionObject: SectionDataObject = { +const sectionObject = { header: 'submission.sections.submit.progressbar.duplicates', - config: 'https://dspace.org/api/config/submissionforms/duplicates', mandatory: true, opened: true, - data: potentialDuplicates, + data: {potentialDuplicates: duplicates}, errorsToShow: [], serverValidationErrors: [], id: 'duplicates', @@ -160,8 +151,7 @@ describe('SubmissionSectionDuplicatesComponent test suite', () => { { provide: 'submissionIdProvider', useValue: submissionId }, { provide: PaginationService, useValue: paginationService }, ChangeDetectorRef, - FormBuilderService, - SubmissionSectionDuplicatesComponent + FormBuilderService ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents().then(); @@ -177,9 +167,9 @@ describe('SubmissionSectionDuplicatesComponent test suite', () => { sectionsServiceStub.isSectionReadOnly.and.returnValue(observableOf(false)); sectionsServiceStub.getSectionErrors.and.returnValue(observableOf([])); sectionsServiceStub.getSectionData.and.returnValue(observableOf(sectionObject)); - const html = ``; - testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testFixture = TestBed.createComponent(SubmissionSectionDuplicatesComponent); testComp = testFixture.componentInstance; + }); afterEach(() => { @@ -187,7 +177,7 @@ describe('SubmissionSectionDuplicatesComponent test suite', () => { }); it('should create SubmissionSectionDuplicatesComponent', () => { - expect(testComp).toBeDefined(); + expect(testComp).toBeTruthy(); }); }); @@ -217,7 +207,7 @@ describe('SubmissionSectionDuplicatesComponent test suite', () => { sectionsServiceStub.isSectionReadOnly.and.returnValue(observableOf(false)); compAsAny.submissionService.getSubmissionScope.and.returnValue(SubmissionScopeType.WorkspaceItem); spyOn(comp, 'getSectionStatus').and.returnValue(observableOf(true)); - spyOn(comp, 'getDuplicateData').and.returnValue(observableOf(potentialDuplicates)); + spyOn(comp, 'getDuplicateData').and.returnValue(observableOf({potentialDuplicates: duplicates})); expect(comp.isLoading).toBeTruthy(); comp.onSectionInit(); fixture.detectChanges(); From aceffa0468610590bf47dbcd0cb750b4fc0c69c2 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 23 Jan 2024 15:54:01 +1300 Subject: [PATCH 05/23] [TLC-674] TypeDoc --- .../sections/duplicates/section-duplicates.component.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/app/submission/sections/duplicates/section-duplicates.component.ts b/src/app/submission/sections/duplicates/section-duplicates.component.ts index 9ac849ea439..c929ab9ce2f 100644 --- a/src/app/submission/sections/duplicates/section-duplicates.component.ts +++ b/src/app/submission/sections/duplicates/section-duplicates.component.ts @@ -104,11 +104,18 @@ export class SubmissionSectionDuplicatesComponent extends SectionModelComponent return observableOf(!this.isLoading); } + /** + * Get duplicate data as observable from the section data + */ public getDuplicateData(): Observable { return this.sectionService.getSectionData(this.submissionId, this.sectionData.id, this.sectionData.sectionType) as Observable; } + /** + * Construct and return an item link for use with a preview item stub + * @param uuid + */ public getItemLink(uuid: any) { return new URLCombiner(getItemModuleRoute(), uuid).toString(); } From e474862edf63ccf15c515f33612533901e896ddc Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 23 Jan 2024 19:16:06 +1300 Subject: [PATCH 06/23] [TLC-674] Lint fixes --- .../submission/models/workspaceitem-sections.model.ts | 4 ++-- .../claimed-search-result-list-element.component.ts | 4 ++-- .../duplicates/section-duplicates.component.spec.ts | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/app/core/submission/models/workspaceitem-sections.model.ts b/src/app/core/submission/models/workspaceitem-sections.model.ts index 9f39c1ab04b..4c90f3ede86 100644 --- a/src/app/core/submission/models/workspaceitem-sections.model.ts +++ b/src/app/core/submission/models/workspaceitem-sections.model.ts @@ -3,9 +3,9 @@ import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.mod import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model'; import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model'; import { WorkspaceitemSectionCcLicenseObject } from './workspaceitem-section-cc-license.model'; -import {WorkspaceitemSectionIdentifiersObject} from './workspaceitem-section-identifiers.model'; +import { WorkspaceitemSectionIdentifiersObject } from './workspaceitem-section-identifiers.model'; import { WorkspaceitemSectionSherpaPoliciesObject } from './workspaceitem-section-sherpa-policies.model'; -import {WorkspaceitemSectionDuplicatesObject} from "./workspaceitem-section-duplicates.model"; +import { WorkspaceitemSectionDuplicatesObject } from './workspaceitem-section-duplicates.model'; /** * An interface to represent submission's section object. 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 fd855193517..ee675405a67 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 @@ -18,11 +18,11 @@ import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interfac import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; import { Item } from '../../../../core/shared/item.model'; -import {map, mergeMap, tap} from 'rxjs/operators'; +import { map, 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'; -import { PaginatedList } from "../../../../core/data/paginated-list.model"; +import { PaginatedList } from '../../../../core/data/paginated-list.model'; @Component({ selector: 'ds-claimed-search-result-list-element', diff --git a/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts b/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts index 4ca1469ebe2..e43851962b6 100644 --- a/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts +++ b/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts @@ -36,9 +36,9 @@ import { ObjNgFor } from '../../../shared/utils/object-ngfor.pipe'; import { VarDirective } from '../../../shared/utils/var.directive'; import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; -import {Duplicate} from "../../../shared/object-list/duplicate-data/duplicate.model"; -import {MetadataValue} from "../../../core/shared/metadata.models"; -import {defaultUUID} from "../../../shared/mocks/uuid.service.mock"; +import { Duplicate} from '../../../shared/object-list/duplicate-data/duplicate.model'; +import { MetadataValue } from '../../../core/shared/metadata.models'; +import { defaultUUID } from '../../../shared/mocks/uuid.service.mock'; function getMockSubmissionFormsConfigService(): SubmissionFormsConfigDataService { return jasmine.createSpyObj('FormOperationsService', { @@ -56,7 +56,7 @@ function getMockCollectionDataService(): CollectionDataService { }); } -const duplicates: Duplicate[]= [{ +const duplicates: Duplicate[] = [{ title: 'Unique title', uuid: defaultUUID, workflowItemId: 1, @@ -72,7 +72,7 @@ const duplicates: Duplicate[]= [{ 'place': 0 })] }, - type: "DUPLICATE" + type: 'DUPLICATE' }]; const sectionObject = { From 140cb885101dc56fc0f445988eea1f720f4aff77 Mon Sep 17 00:00:00 2001 From: Pascal-Nicolas Becker Date: Tue, 23 Jan 2024 15:57:34 +0100 Subject: [PATCH 07/23] Tiny code style fix in pool-search-result-list-element.component.ts --- .../pool-search-result-list-element.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts index 8a93d52e9ff..5161faee89e 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts @@ -1,7 +1,7 @@ import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { BehaviorSubject, EMPTY, Observable } from 'rxjs'; -import {map, mergeMap, tap} from 'rxjs/operators'; +import { map, mergeMap, tap } from 'rxjs/operators'; import { ViewMode } from '../../../../core/shared/view-mode.model'; import { RemoteData } from '../../../../core/data/remote-data'; From db8d47e598827ce4895a4839e8217a7419bc8ebb Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Thu, 1 Feb 2024 11:29:16 +1300 Subject: [PATCH 08/23] [TLC-674] Refactor duplicates from item link to searchBy --- src/app/core/data/item-data.service.ts | 35 +++++++++---------- src/app/core/shared/item.model.ts | 5 --- .../duplicate-data/duplicate.model.ts | 21 +++++++++-- ...arch-result-list-element.component.spec.ts | 13 +++++-- ...ed-search-result-list-element.component.ts | 4 ++- ...arch-result-list-element.component.spec.ts | 13 +++++-- ...ol-search-result-list-element.component.ts | 6 ++-- 7 files changed, 64 insertions(+), 33 deletions(-) diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index f2af44401a7..1a0d181ca1d 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -46,6 +46,9 @@ import { RestRequestMethod } from './rest-request-method'; import { CreateData, CreateDataImpl } from './base/create-data'; import { RequestParam } from '../cache/models/request-param.model'; import { dataService } from './base/data-service.decorator'; +import { Duplicate } from '../../shared/object-list/duplicate-data/duplicate.model'; +import { SearchDataImpl } from './base/search-data'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; /** * An abstract service for CRUD operations on Items @@ -56,6 +59,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService private createData: CreateData; private patchData: PatchData; private deleteData: DeleteData; + private searchData: SearchDataImpl; protected constructor( protected linkPath, @@ -74,6 +78,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService this.createData = new CreateDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive); this.patchData = new PatchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, comparator, this.responseMsToLive, this.constructIdEndpoint); this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint); + this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); } /** @@ -242,24 +247,18 @@ 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); + public findDuplicates(uuid: string, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + const searchParams = [new RequestParam('uuid', uuid)]; + let findListOptions = new FindListOptions(); + if (options) { + findListOptions = Object.assign(new FindListOptions(), options); + } + if (findListOptions.searchParams) { + findListOptions.searchParams = [...findListOptions.searchParams, ...searchParams]; + } else { + findListOptions.searchParams = searchParams; + } + return this.searchData.searchBy('findDuplicates', findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } /** diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index 5ef670c42b9..20fc275ee28 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -26,7 +26,6 @@ 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 @@ -80,7 +79,6 @@ export class Item extends DSpaceObject implements ChildHALResource, HandleObject thumbnail: HALLink; accessStatus: HALLink; identifiers: HALLink; - duplicates: HALLink; self: HALLink; }; @@ -133,9 +131,6 @@ 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/shared/object-list/duplicate-data/duplicate.model.ts b/src/app/shared/object-list/duplicate-data/duplicate.model.ts index c5cd1705e1b..cbcff155e10 100644 --- a/src/app/shared/object-list/duplicate-data/duplicate.model.ts +++ b/src/app/shared/object-list/duplicate-data/duplicate.model.ts @@ -1,7 +1,14 @@ -import { autoserialize } from 'cerialize'; +import {autoserialize, deserialize} from 'cerialize'; import { MetadataMap } from '../../../core/shared/metadata.models'; +import { HALLink} from '../../../core/shared/hal-link.model'; +import { CacheableObject } from '../../../core/cache/cacheable-object.model'; +import { DUPLICATE } from './duplicate.resource-type'; +import { ResourceType } from '../../../core/shared/resource-type'; + +export class Duplicate implements CacheableObject { + + static type = DUPLICATE; -export class Duplicate { /** * The item title */ @@ -23,5 +30,13 @@ export class Duplicate { metadata: MetadataMap; @autoserialize - type: string; + type: ResourceType; + + /** + * The {@link HALLink}s for this Bitstream + */ + @deserialize + _links: { + self: HALLink; + }; } diff --git a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts index c1778c1792b..6c4616ea2e8 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts @@ -15,7 +15,7 @@ import { Item } from '../../../../core/shared/item.model'; import { ClaimedSearchResultListElementComponent } from './claimed-search-result-list-element.component'; import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model'; import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model'; -import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils'; import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model'; import { TruncatableService } from '../../../truncatable/truncatable.service'; import { VarDirective } from '../../../utils/var.directive'; @@ -28,6 +28,8 @@ import { APP_CONFIG } from '../../../../../config/app-config.interface'; import { environment } from '../../../../../environments/environment'; import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; import { Context } from '../../../../core/shared/context.model'; +import { createPaginatedList } from '../../../testing/utils.test'; +import { ItemDataService } from '../../../../core/data/item-data.service'; let component: ClaimedSearchResultListElementComponent; let fixture: ComponentFixture; @@ -35,6 +37,12 @@ let fixture: ComponentFixture; const mockResultObject: ClaimedTaskSearchResult = new ClaimedTaskSearchResult(); mockResultObject.hitHighlights = {}; +const emptyList = createSuccessfulRemoteDataObject(createPaginatedList([])); +const itemDataServiceStub = { + findDuplicates: () => createSuccessfulRemoteDataObject$({}), + findListByHref: () => observableOf(emptyList), +}; + const item = Object.assign(new Item(), { bundles: observableOf({}), metadata: { @@ -83,7 +91,8 @@ describe('ClaimedSearchResultListElementComponent', () => { { provide: LinkService, useValue: linkService }, { provide: DSONameService, useClass: DSONameServiceMock }, { provide: APP_CONFIG, useValue: environment }, - { provide: ObjectCacheService, useValue: objectCacheServiceMock } + { provide: ObjectCacheService, useValue: objectCacheServiceMock }, + { provide: ItemDataService, useValue: itemDataServiceStub }, ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(ClaimedSearchResultListElementComponent, { 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 ee675405a67..77d83ffe38a 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 @@ -23,6 +23,7 @@ import { isNotEmpty, hasValue } from '../../../empty.util'; import { Context } from '../../../../core/shared/context.model'; import { Duplicate } from '../../duplicate-data/duplicate.model'; import { PaginatedList } from '../../../../core/data/paginated-list.model'; +import { ItemDataService } from '../../../../core/data/item-data.service'; @Component({ selector: 'ds-claimed-search-result-list-element', @@ -67,6 +68,7 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle protected truncatableService: TruncatableService, public dsoNameService: DSONameService, protected objectCache: ObjectCacheService, + protected itemDataService: ItemDataService, @Inject(APP_CONFIG) protected appConfig: AppConfig ) { super(truncatableService, dsoNameService, appConfig); @@ -97,7 +99,7 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle tap((itemRD: RemoteData) => { if (isNotEmpty(itemRD) && itemRD.hasSucceeded) { this.item$.next(itemRD.payload); - this.duplicates$ = itemRD.payload.duplicates.pipe( + this.duplicates$ = this.itemDataService.findDuplicates(itemRD.payload.uuid).pipe( getFirstCompletedRemoteData(), map((remoteData: RemoteData>) => { if (remoteData.hasSucceeded) { diff --git a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts index 9aad4a8b7b9..02e375b37d6 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts @@ -15,7 +15,7 @@ import { Item } from '../../../../core/shared/item.model'; import { PoolSearchResultListElementComponent } from './pool-search-result-list-element.component'; import { PoolTask } from '../../../../core/tasks/models/pool-task-object.model'; import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model'; -import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils'; import { PoolTaskSearchResult } from '../../../object-collection/shared/pool-task-search-result.model'; import { TruncatableService } from '../../../truncatable/truncatable.service'; import { VarDirective } from '../../../utils/var.directive'; @@ -27,6 +27,8 @@ import { DSONameServiceMock } from '../../../mocks/dso-name.service.mock'; import { APP_CONFIG } from '../../../../../config/app-config.interface'; import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; import { Context } from '../../../../core/shared/context.model'; +import { createPaginatedList } from '../../../testing/utils.test'; +import { ItemDataService } from '../../../../core/data/item-data.service'; let component: PoolSearchResultListElementComponent; let fixture: ComponentFixture; @@ -34,6 +36,12 @@ let fixture: ComponentFixture; const mockResultObject: PoolTaskSearchResult = new PoolTaskSearchResult(); mockResultObject.hitHighlights = {}; +const emptyList = createSuccessfulRemoteDataObject(createPaginatedList([])); +const itemDataServiceStub = { + findDuplicates: () => createSuccessfulRemoteDataObject$({}), + findListByHref: () => observableOf(emptyList), +}; + const item = Object.assign(new Item(), { duplicates: observableOf([]), bundles: observableOf({}), @@ -90,7 +98,8 @@ describe('PoolSearchResultListElementComponent', () => { { provide: LinkService, useValue: linkService }, { provide: DSONameService, useClass: DSONameServiceMock }, { provide: APP_CONFIG, useValue: environmentUseThumbs }, - { provide: ObjectCacheService, useValue: objectCacheServiceMock } + { provide: ObjectCacheService, useValue: objectCacheServiceMock }, + { provide: ItemDataService, useValue: itemDataServiceStub }, ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(PoolSearchResultListElementComponent, { diff --git a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts index 5161faee89e..e9e8088757c 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts @@ -24,6 +24,7 @@ import { isNotEmpty, hasValue } from '../../../empty.util'; import { Context } from '../../../../core/shared/context.model'; import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { Duplicate } from '../../duplicate-data/duplicate.model'; +import { ItemDataService } from '../../../../core/data/item-data.service'; /** * This component renders pool task object for the search result in the list view. @@ -77,6 +78,7 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen protected truncatableService: TruncatableService, public dsoNameService: DSONameService, protected objectCache: ObjectCacheService, + protected itemDataService: ItemDataService, @Inject(APP_CONFIG) protected appConfig: AppConfig ) { super(truncatableService, dsoNameService, appConfig); @@ -88,7 +90,7 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen ngOnInit() { super.ngOnInit(); this.linkService.resolveLinks(this.dso, followLink('workflowitem', {}, - followLink('item', {}, followLink('bundles'), followLink('duplicates')), + followLink('item', {}, followLink('bundles')), followLink('submitter') ), followLink('action')); @@ -107,7 +109,7 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen tap((itemRD: RemoteData) => { if (isNotEmpty(itemRD) && itemRD.hasSucceeded) { this.item$.next(itemRD.payload); - this.duplicates$ = itemRD.payload.duplicates.pipe( + this.duplicates$ = this.itemDataService.findDuplicates(itemRD.payload.uuid).pipe( getFirstCompletedRemoteData(), map((remoteData: RemoteData>) => { if (remoteData.hasSucceeded) { From b8e2cac3995fa41c0ba9d82677096ab0a49ce9b0 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Thu, 1 Feb 2024 11:49:56 +1300 Subject: [PATCH 09/23] [TLC-674] Fix duplicate section unit test --- .../duplicates/section-duplicates.component.spec.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts b/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts index e43851962b6..5d00db41596 100644 --- a/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts +++ b/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts @@ -39,6 +39,7 @@ import { PaginationServiceStub } from '../../../shared/testing/pagination-servic import { Duplicate} from '../../../shared/object-list/duplicate-data/duplicate.model'; import { MetadataValue } from '../../../core/shared/metadata.models'; import { defaultUUID } from '../../../shared/mocks/uuid.service.mock'; +import { DUPLICATE } from '../../../shared/object-list/duplicate-data/duplicate.resource-type'; function getMockSubmissionFormsConfigService(): SubmissionFormsConfigDataService { return jasmine.createSpyObj('FormOperationsService', { @@ -72,7 +73,12 @@ const duplicates: Duplicate[] = [{ 'place': 0 })] }, - type: 'DUPLICATE' + type: DUPLICATE, + _links: { + self: { + href: 'http://localhost:8080/server/api/core/items/search/findDuplicates?uuid=testid' + } + } }]; const sectionObject = { From 7a10834f82c9e646cd7e53dd3050bcb187fe01ae Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Sun, 11 Feb 2024 12:21:56 +1300 Subject: [PATCH 10/23] [TLC-674] Dupe layout changes as per review i18n fix alignment fix comment fix --- .../duplicate-data/duplicate.resource-type.ts | 2 +- .../claimed-search-result-list-element.component.html | 9 ++++++--- .../pool-search-result-list-element.component.html | 9 ++++++--- .../duplicates/section-duplicates.component.html | 2 +- 4 files changed, 14 insertions(+), 8 deletions(-) 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 index bbaba8f79d4..588ca2da55e 100644 --- a/src/app/shared/object-list/duplicate-data/duplicate.resource-type.ts +++ b/src/app/shared/object-list/duplicate-data/duplicate.resource-type.ts @@ -1,7 +1,7 @@ import { ResourceType } from 'src/app/core/shared/resource-type'; /** - * The resource type for Access Status + * The resource type for Duplicate preview stubs * * Needs to be in a separate file to prevent circular * dependencies in webpack. diff --git a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.html b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.html index dd9fec79d30..e93e27b41c8 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.html +++ b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.html @@ -6,9 +6,12 @@ [workflowItem]="workflowitem$.value"> -
-
- {{ duplicateCount }} {{ 'submission.workflow.tasks.duplicates' | translate }} +
+
+
+
+ {{ duplicateCount }} {{ 'submission.workflow.tasks.duplicates' | translate }} +
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 d80bcc9b4c2..932ade0e07d 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 @@ -6,9 +6,12 @@ [workflowItem]="workflowitem$.value"> -
-
- {{ duplicateCount }} {{ 'submission.workflow.tasks.duplicates' | translate }} +
+
+
+
+ {{ duplicateCount }} {{ 'submission.workflow.tasks.duplicates' | translate }} +
diff --git a/src/app/submission/sections/duplicates/section-duplicates.component.html b/src/app/submission/sections/duplicates/section-duplicates.component.html index 285c6b2b68a..7634dbb4f7d 100644 --- a/src/app/submission/sections/duplicates/section-duplicates.component.html +++ b/src/app/submission/sections/duplicates/section-duplicates.component.html @@ -4,7 +4,7 @@ -->
-

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

+

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

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

From b4591b1372c86025bb039f3ad65b2dbec93b5b1d Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Mon, 19 Feb 2024 15:08:45 +1300 Subject: [PATCH 11/23] [TLC-674] Potential duplicate msg shown as warning alert --- .../claimed-search-result-list-element.component.html | 2 +- .../pool-search-result-list-element.component.html | 2 +- .../sections/duplicates/section-duplicates.component.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.html b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.html index e93e27b41c8..9cdeb49e5c6 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.html +++ b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.html @@ -9,7 +9,7 @@
-
+
{{ duplicateCount }} {{ 'submission.workflow.tasks.duplicates' | translate }}
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 932ade0e07d..a2b73eb761c 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 @@ -9,7 +9,7 @@
-
+
{{ duplicateCount }} {{ 'submission.workflow.tasks.duplicates' | translate }}
diff --git a/src/app/submission/sections/duplicates/section-duplicates.component.html b/src/app/submission/sections/duplicates/section-duplicates.component.html index 7634dbb4f7d..6354695ca8b 100644 --- a/src/app/submission/sections/duplicates/section-duplicates.component.html +++ b/src/app/submission/sections/duplicates/section-duplicates.component.html @@ -7,7 +7,7 @@

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

-

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

+
{{ 'submission.sections.duplicates.detected' | translate }}
{{dupe.title}}
From a9a87d31ef837ebcefbe8c895996ee684624f536 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Mon, 19 Feb 2024 15:14:04 +1300 Subject: [PATCH 12/23] [TLC-674] Allow for no duplicate section in submitted workspace item --- .../sections/duplicates/section-duplicates.component.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/submission/sections/duplicates/section-duplicates.component.html b/src/app/submission/sections/duplicates/section-duplicates.component.html index 6354695ca8b..a6c3abd9814 100644 --- a/src/app/submission/sections/duplicates/section-duplicates.component.html +++ b/src/app/submission/sections/duplicates/section-duplicates.component.html @@ -3,12 +3,12 @@ @author Kim Shepherd -->
- +

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

- +
{{ 'submission.sections.duplicates.detected' | translate }}
-
+
{{dupe.title}}
{{('item.preview.' + metadatum.key) | translate}} {{metadatum.value}} From 68dd35095c4ad44331edfc9d7c6f88c92b8fb4a7 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Mon, 19 Feb 2024 15:56:08 +1300 Subject: [PATCH 13/23] [TLC-674] Do not display duplicate section if data is empty --- .../objects/submission-objects.effects.ts | 72 +++++++++++-------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/src/app/submission/objects/submission-objects.effects.ts b/src/app/submission/objects/submission-objects.effects.ts index 2c19224336d..c34e8c908e2 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -1,25 +1,25 @@ -import { Injectable } from '@angular/core'; -import { Actions, createEffect, ofType } from '@ngrx/effects'; -import { Store } from '@ngrx/store'; -import { TranslateService } from '@ngx-translate/core'; +import {Injectable} from '@angular/core'; +import {Actions, createEffect, ofType} from '@ngrx/effects'; +import {Store} from '@ngrx/store'; +import {TranslateService} from '@ngx-translate/core'; import findKey from 'lodash/findKey'; import isEqual from 'lodash/isEqual'; import union from 'lodash/union'; -import { from as observableFrom, Observable, of as observableOf } from 'rxjs'; -import { catchError, filter, map, mergeMap, switchMap, take, tap, withLatestFrom } from 'rxjs/operators'; -import { SubmissionObject } from '../../core/submission/models/submission-object.model'; -import { WorkflowItem } from '../../core/submission/models/workflowitem.model'; -import { WorkspaceitemSectionUploadObject } from '../../core/submission/models/workspaceitem-section-upload.model'; -import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model'; -import { WorkspaceItem } from '../../core/submission/models/workspaceitem.model'; -import { SubmissionJsonPatchOperationsService } from '../../core/submission/submission-json-patch-operations.service'; -import { isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { SectionsType } from '../sections/sections-type'; -import { SectionsService } from '../sections/sections.service'; -import { SubmissionState } from '../submission.reducers'; -import { SubmissionService } from '../submission.service'; +import {from as observableFrom, Observable, of as observableOf} from 'rxjs'; +import {catchError, filter, map, mergeMap, switchMap, take, tap, withLatestFrom} from 'rxjs/operators'; +import {SubmissionObject} from '../../core/submission/models/submission-object.model'; +import {WorkflowItem} from '../../core/submission/models/workflowitem.model'; +import {WorkspaceitemSectionUploadObject} from '../../core/submission/models/workspaceitem-section-upload.model'; +import {WorkspaceitemSectionsObject} from '../../core/submission/models/workspaceitem-sections.model'; +import {WorkspaceItem} from '../../core/submission/models/workspaceitem.model'; +import {SubmissionJsonPatchOperationsService} from '../../core/submission/submission-json-patch-operations.service'; +import {isEmpty, isNotEmpty, isNotUndefined} from '../../shared/empty.util'; +import {NotificationsService} from '../../shared/notifications/notifications.service'; +import {SectionsType} from '../sections/sections-type'; +import {SectionsService} from '../sections/sections.service'; +import {SubmissionState} from '../submission.reducers'; +import {SubmissionService} from '../submission.service'; import parseSectionErrors from '../utils/parseSectionErrors'; import { CompleteInitSubmissionFormAction, @@ -45,16 +45,19 @@ import { UpdateSectionDataAction, UpdateSectionDataSuccessAction } from './submission-objects.actions'; -import { SubmissionObjectEntry } from './submission-objects.reducer'; -import { Item } from '../../core/shared/item.model'; -import { RemoteData } from '../../core/data/remote-data'; -import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; -import { SubmissionObjectDataService } from '../../core/submission/submission-object-data.service'; -import { followLink } from '../../shared/utils/follow-link-config.model'; -import parseSectionErrorPaths, { SectionErrorPath } from '../utils/parseSectionErrorPaths'; -import { FormState } from '../../shared/form/form.reducer'; -import { SubmissionSectionObject } from './submission-section-object.model'; -import { SubmissionSectionError } from './submission-section-error.model'; +import {SubmissionObjectEntry} from './submission-objects.reducer'; +import {Item} from '../../core/shared/item.model'; +import {RemoteData} from '../../core/data/remote-data'; +import {getFirstSucceededRemoteDataPayload} from '../../core/shared/operators'; +import {SubmissionObjectDataService} from '../../core/submission/submission-object-data.service'; +import {followLink} from '../../shared/utils/follow-link-config.model'; +import parseSectionErrorPaths, {SectionErrorPath} from '../utils/parseSectionErrorPaths'; +import {FormState} from '../../shared/form/form.reducer'; +import {SubmissionSectionObject} from './submission-section-object.model'; +import {SubmissionSectionError} from './submission-section-error.model'; +import { + WorkspaceitemSectionDuplicatesObject +} from '../../core/submission/models/workspaceitem-section-duplicates.model'; @Injectable() export class SubmissionObjectEffects { @@ -71,7 +74,11 @@ export class SubmissionObjectEffects { const selfLink = sectionDefinition._links.self.href || sectionDefinition._links.self; const sectionId = selfLink.substr(selfLink.lastIndexOf('/') + 1); const config = sectionDefinition._links.config ? (sectionDefinition._links.config.href || sectionDefinition._links.config) : ''; - const enabled = (sectionDefinition.mandatory) || (isNotEmpty(action.payload.sections) && action.payload.sections.hasOwnProperty(sectionId)); + // A section is enabled if it is mandatory (except duplicate detection) or contains data in its section payload + const enabled = (sectionDefinition.mandatory && (sectionDefinition.sectionType !== SectionsType.Duplicates)) + || (isNotEmpty(action.payload.sections) && action.payload.sections.hasOwnProperty(sectionId) + && (sectionDefinition.sectionType === SectionsType.Duplicates && isNotEmpty((action.payload.sections[sectionId] as WorkspaceitemSectionDuplicatesObject).potentialDuplicates)) + ); let sectionData; if (sectionDefinition.sectionType !== SectionsType.SubmissionForm) { sectionData = (isNotUndefined(action.payload.sections) && isNotUndefined(action.payload.sections[sectionId])) ? action.payload.sections[sectionId] : Object.create(null); @@ -434,8 +441,13 @@ export class SubmissionObjectEffects { && isEmpty(sections[sherpaPoliciesSectionId])) { mappedActions.push(new UpdateSectionDataAction(submissionId, sherpaPoliciesSectionId, null, [], [])); } - }); + // When Duplicate Detection step is enabled, add it only if there are duplicates + const duplicatesSectionId = findKey(currentState.sections, (section) => section.sectionType === SectionsType.Duplicates); + if (isNotUndefined(duplicatesSectionId) && isNotEmpty(currentState.sections[duplicatesSectionId]?.data) && isEmpty(sections[duplicatesSectionId])) { + mappedActions.push(new UpdateSectionDataAction(submissionId, duplicatesSectionId, null, [], [])); + } + }); } return mappedActions; } From 911cf8905c8c4a61172d81951f86850d3653e46d Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Mon, 19 Feb 2024 17:03:31 +1300 Subject: [PATCH 14/23] [TLC-674] Hide empty duplicate section UNLESS config overrides A new config property allows the user to force the duplicate section to be displayed even if there are no duplicates as sometimes this is useful information to a reviewer or submitter --- config/config.example.yml | 4 ++++ .../objects/submission-objects.actions.ts | 22 +++++++++++++++++ .../objects/submission-objects.effects.ts | 24 +++++++++++++------ .../objects/submission-objects.reducer.ts | 23 +++++++++++++++++- .../section-duplicates.component.html | 2 +- src/config/config.util.spec.ts | 1 + src/config/default-app-config.ts | 3 +++ src/config/submission-config.interface.ts | 5 ++++ src/environments/environment.test.ts | 3 +++ 9 files changed, 78 insertions(+), 9 deletions(-) diff --git a/config/config.example.yml b/config/config.example.yml index 8b010ba6ea6..6a35fb40f1e 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -131,6 +131,10 @@ submission: # NOTE: after how many time (milliseconds) submission is saved automatically # eg. timer: 5 * (1000 * 60); // 5 minutes timer: 0 + # Always show the duplicate detection section if enabled, even if there are no potential duplicates detected + # (a message will be displayed to indicate no matches were found) + duplicateDetection: + alwaysShowSection: false icons: metadata: # NOTE: example of configuration diff --git a/src/app/submission/objects/submission-objects.actions.ts b/src/app/submission/objects/submission-objects.actions.ts index 86d90f05f30..24d9378f353 100644 --- a/src/app/submission/objects/submission-objects.actions.ts +++ b/src/app/submission/objects/submission-objects.actions.ts @@ -56,6 +56,9 @@ export const SubmissionObjectActionTypes = { DISCARD_SUBMISSION_SUCCESS: type('dspace/submission/DISCARD_SUBMISSION_SUCCESS'), DISCARD_SUBMISSION_ERROR: type('dspace/submission/DISCARD_SUBMISSION_ERROR'), + // Clearing active section types + CLEAN_DUPLICATE_DETECTION: type('dspace/submission/CLEAN_DUPLICATE_DETECTION'), + // Upload file types NEW_FILE: type('dspace/submission/NEW_FILE'), EDIT_FILE_DATA: type('dspace/submission/EDIT_FILE_DATA'), @@ -240,6 +243,25 @@ export class UpdateSectionDataAction implements Action { } } +/** + * Removes data and makes 'detect-duplicate' section not visible. + */ +export class CleanDuplicateDetectionAction implements Action { + type = SubmissionObjectActionTypes.CLEAN_DUPLICATE_DETECTION; + payload: { + submissionId: string; + }; + + /** + * creates a new CleanDetectDuplicateAction + * + * @param submissionId Id of the submission on which perform the action + */ + constructor(submissionId: string ) { + this.payload = { submissionId }; + } +} + export class UpdateSectionDataSuccessAction implements Action { type = SubmissionObjectActionTypes.UPDATE_SECTION_DATA_SUCCESS; } diff --git a/src/app/submission/objects/submission-objects.effects.ts b/src/app/submission/objects/submission-objects.effects.ts index c34e8c908e2..c4fd42b0d32 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -43,7 +43,8 @@ import { SubmissionObjectAction, SubmissionObjectActionTypes, UpdateSectionDataAction, - UpdateSectionDataSuccessAction + UpdateSectionDataSuccessAction, + CleanDuplicateDetectionAction } from './submission-objects.actions'; import {SubmissionObjectEntry} from './submission-objects.reducer'; import {Item} from '../../core/shared/item.model'; @@ -58,6 +59,7 @@ import {SubmissionSectionError} from './submission-section-error.model'; import { WorkspaceitemSectionDuplicatesObject } from '../../core/submission/models/workspaceitem-section-duplicates.model'; +import { environment } from '../../../environments/environment'; @Injectable() export class SubmissionObjectEffects { @@ -74,10 +76,11 @@ export class SubmissionObjectEffects { const selfLink = sectionDefinition._links.self.href || sectionDefinition._links.self; const sectionId = selfLink.substr(selfLink.lastIndexOf('/') + 1); const config = sectionDefinition._links.config ? (sectionDefinition._links.config.href || sectionDefinition._links.config) : ''; - // A section is enabled if it is mandatory (except duplicate detection) or contains data in its section payload + // A section is enabled if it is mandatory or contains data in its section payload + // except for detect duplicate steps which will be hidden with no data unless overridden in config, even if mandatory const enabled = (sectionDefinition.mandatory && (sectionDefinition.sectionType !== SectionsType.Duplicates)) || (isNotEmpty(action.payload.sections) && action.payload.sections.hasOwnProperty(sectionId) - && (sectionDefinition.sectionType === SectionsType.Duplicates && isNotEmpty((action.payload.sections[sectionId] as WorkspaceitemSectionDuplicatesObject).potentialDuplicates)) + && (sectionDefinition.sectionType === SectionsType.Duplicates && (alwaysDisplayDuplicates() || isNotEmpty((action.payload.sections[sectionId] as WorkspaceitemSectionDuplicatesObject).potentialDuplicates))) ); let sectionData; if (sectionDefinition.sectionType !== SectionsType.SubmissionForm) { @@ -442,10 +445,13 @@ export class SubmissionObjectEffects { mappedActions.push(new UpdateSectionDataAction(submissionId, sherpaPoliciesSectionId, null, [], [])); } - // When Duplicate Detection step is enabled, add it only if there are duplicates - const duplicatesSectionId = findKey(currentState.sections, (section) => section.sectionType === SectionsType.Duplicates); - if (isNotUndefined(duplicatesSectionId) && isNotEmpty(currentState.sections[duplicatesSectionId]?.data) && isEmpty(sections[duplicatesSectionId])) { - mappedActions.push(new UpdateSectionDataAction(submissionId, duplicatesSectionId, null, [], [])); + // When Duplicate Detection step is enabled, add it only if there are duplicates in the response section data + // or if configuration overrides this behaviour + if (!alwaysDisplayDuplicates()) { + const duplicatesSectionId = findKey(currentState.sections, (section) => section.sectionType === SectionsType.Duplicates); + if (isNotUndefined(duplicatesSectionId) && isEmpty((sections[duplicatesSectionId] as WorkspaceitemSectionDuplicatesObject).potentialDuplicates)) { + mappedActions.push(new CleanDuplicateDetectionAction(submissionId)); + } } }); } @@ -493,3 +499,7 @@ function filterErrors(sectionForm: FormState, sectionErrors: SubmissionSectionEr }); return filteredErrors; } + +function alwaysDisplayDuplicates(): boolean { + return (environment.submission.duplicateDetection.alwaysShowSection); +} diff --git a/src/app/submission/objects/submission-objects.reducer.ts b/src/app/submission/objects/submission-objects.reducer.ts index 4970e25d325..c81f98db6a0 100644 --- a/src/app/submission/objects/submission-objects.reducer.ts +++ b/src/app/submission/objects/submission-objects.reducer.ts @@ -5,7 +5,7 @@ import isEqual from 'lodash/isEqual'; import uniqWith from 'lodash/uniqWith'; import { - ChangeSubmissionCollectionAction, + ChangeSubmissionCollectionAction, CleanDuplicateDetectionAction, CompleteInitSubmissionFormAction, DeleteSectionErrorsAction, DeleteUploadedFileAction, @@ -229,6 +229,10 @@ export function submissionObjectReducer(state = initialState, action: Submission return removeSectionErrors(state, action as RemoveSectionErrorsAction); } + case SubmissionObjectActionTypes.CLEAN_DUPLICATE_DETECTION: { + return cleanDuplicateDetectionSection(state, action as CleanDuplicateDetectionAction); + } + default: { return state; } @@ -856,3 +860,20 @@ function deleteFile(state: SubmissionObjectState, action: DeleteUploadedFileActi } return state; } + +function cleanDuplicateDetectionSection(state: SubmissionObjectState, action: CleanDuplicateDetectionAction): SubmissionObjectState { + if (isNotEmpty(state[ action.payload.submissionId ])) { + return Object.assign({}, state, { + [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { + sections: Object.assign({}, state[ action.payload.submissionId ].sections, { + [ 'duplicates' ]: Object.assign({}, state[ action.payload.submissionId ].sections.duplicates, { + enabled: false, + data: { potentialDuplicates: [] } + }) + }) + }) + }); + } else { + return state; + } +} diff --git a/src/app/submission/sections/duplicates/section-duplicates.component.html b/src/app/submission/sections/duplicates/section-duplicates.component.html index a6c3abd9814..78c9e5df282 100644 --- a/src/app/submission/sections/duplicates/section-duplicates.component.html +++ b/src/app/submission/sections/duplicates/section-duplicates.component.html @@ -4,7 +4,7 @@ -->
-

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

+
{{ 'submission.sections.duplicates.none' | translate }}
{{ 'submission.sections.duplicates.detected' | translate }}
diff --git a/src/config/config.util.spec.ts b/src/config/config.util.spec.ts index 4dc2b67260e..cbf3bb329f3 100644 --- a/src/config/config.util.spec.ts +++ b/src/config/config.util.spec.ts @@ -13,6 +13,7 @@ describe('Config Util', () => { expect(appConfig.ui.useProxies).toEqual(true); expect(appConfig.submission.autosave.metadata).toEqual([]); + expect(appConfig.submission.duplicateDetection.alwaysShowSection).toEqual(false); expect(appConfig.themes.length).toEqual(1); expect(appConfig.themes[0].name).toEqual('dspace'); diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index 9ba5ee9a350..5283dea4e1c 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -154,6 +154,9 @@ export class DefaultAppConfig implements AppConfig { */ timer: 0 }, + duplicateDetection: { + alwaysShowSection: false + }, typeBind: { field: 'dc.type' }, diff --git a/src/config/submission-config.interface.ts b/src/config/submission-config.interface.ts index b0d1df900fa..afc81a39e25 100644 --- a/src/config/submission-config.interface.ts +++ b/src/config/submission-config.interface.ts @@ -5,6 +5,10 @@ interface AutosaveConfig extends Config { timer: number; } +interface DuplicateDetectionConfig extends Config { + alwaysShowSection: boolean; +} + interface TypeBindConfig extends Config { field: string; } @@ -29,6 +33,7 @@ export interface ConfidenceIconConfig extends Config { export interface SubmissionConfig extends Config { autosave: AutosaveConfig; + duplicateDetection: DuplicateDetectionConfig; typeBind: TypeBindConfig; icons: IconsConfig; } diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index 6f3ab32fa91..11354ca439b 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -121,6 +121,9 @@ export const environment: BuildConfig = { // NOTE: every how many minutes submission is saved automatically timer: 5 }, + duplicateDetection: { + alwaysShowSection: false + }, typeBind: { field: 'dc.type' }, From 0d42af38cd400f24c2c8c126c03d062d67c0bbfe Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Mon, 26 Feb 2024 15:39:45 +1300 Subject: [PATCH 15/23] [TLC-674] Refactor Basic Duplicate detection endpoint / data service --- src/app/core/core.module.ts | 2 + src/app/core/data/duplicate-search.service.ts | 101 ++++++++++++++++++ ...ed-search-result-list-element.component.ts | 4 +- ...ol-search-result-list-element.component.ts | 7 +- 4 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 src/app/core/data/duplicate-search.service.ts diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 7088860674a..8a0edcf8022 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -198,6 +198,7 @@ import { NotifyRequestsStatus } from '../item-page/simple/notify-requests-status import { LdnService } from '../admin/admin-ldn-services/ldn-services-model/ldn-services.model'; import { Itemfilter } from '../admin/admin-ldn-services/ldn-services-model/ldn-service-itemfilters'; import { SubmissionCoarNotifyConfig } from '../submission/sections/section-coar-notify/submission-coar-notify.config'; +import { DuplicateDataService } from './data/duplicate-search.service'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -234,6 +235,7 @@ const PROVIDERS = [ HALEndpointService, HostWindowService, ItemDataService, + DuplicateDataService, MetadataService, ObjectCacheService, PaginationComponentOptions, diff --git a/src/app/core/data/duplicate-search.service.ts b/src/app/core/data/duplicate-search.service.ts new file mode 100644 index 00000000000..f33188119af --- /dev/null +++ b/src/app/core/data/duplicate-search.service.ts @@ -0,0 +1,101 @@ +/* eslint-disable max-classes-per-file */ +import { Observable } from 'rxjs'; +import { Injectable } from '@angular/core'; +import { map } from 'rxjs/operators'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { ResponseParsingService } from './parsing.service'; +import { RemoteData } from './remote-data'; +import { GetRequest } from './request.models'; +import { RequestService } from './request.service'; +import { GenericConstructor } from '../shared/generic-constructor'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { SearchResponseParsingService } from './search-response-parsing.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { RestRequest } from './rest-request.model'; +import { BaseDataService } from './base/base-data.service'; +import { FindListOptions } from './find-list-options.model'; +import { Duplicate } from '../../shared/object-list/duplicate-data/duplicate.model'; +import { PaginatedList } from './paginated-list.model'; +import { RequestParam } from '../cache/models/request-param.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; + + +/** + * Service that performs all general actions that have to do with the search page + */ +@Injectable() +export class DuplicateDataService extends BaseDataService { + + /** + * The ResponseParsingService constructor name + */ + private parser: GenericConstructor = SearchResponseParsingService; + + /** + * The RestRequest constructor name + */ + private request: GenericConstructor = GetRequest; + + /** + * Subscription to unsubscribe from + */ + private sub; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + ) { + super('duplicates', requestService, rdbService, objectCache, halService); + } + + protected getEndpoint(): Observable { + return this.halService.getEndpoint(this.linkPath); + } + + /** + * Method to set service options + * @param {GenericConstructor} parser The ResponseParsingService constructor name + * @param {boolean} request The RestRequest constructor name + */ + setServiceOptions(parser: GenericConstructor, request: GenericConstructor) { + if (parser) { + this.parser = parser; + } + if (request) { + this.request = request; + } + } + + private getSearchUrl(): Observable { + const href$ = this.getEndpoint(); + return href$.pipe( + map((href) => href + '/search') + ); + } + + public findDuplicates(uuid: string, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + const searchParams = [new RequestParam('uuid', uuid)]; + let findListOptions = new FindListOptions(); + if (options) { + findListOptions = Object.assign(new FindListOptions(), options); + } + if (findListOptions.searchParams) { + findListOptions.searchParams = [...findListOptions.searchParams, ...searchParams]; + } else { + findListOptions.searchParams = searchParams; + } + + return this.findListByHref(this.getSearchUrl(), findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } + + /** + * Unsubscribe from the subscription + */ + ngOnDestroy(): void { + if (this.sub !== undefined) { + this.sub.unsubscribe(); + } + } +} 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 77d83ffe38a..92adbc28ca6 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 @@ -24,6 +24,7 @@ import { Context } from '../../../../core/shared/context.model'; import { Duplicate } from '../../duplicate-data/duplicate.model'; import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { ItemDataService } from '../../../../core/data/item-data.service'; +import { DuplicateDataService } from '../../../../core/data/duplicate-search.service'; @Component({ selector: 'ds-claimed-search-result-list-element', @@ -69,6 +70,7 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle public dsoNameService: DSONameService, protected objectCache: ObjectCacheService, protected itemDataService: ItemDataService, + protected duplicateDataService: DuplicateDataService, @Inject(APP_CONFIG) protected appConfig: AppConfig ) { super(truncatableService, dsoNameService, appConfig); @@ -99,7 +101,7 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle tap((itemRD: RemoteData) => { if (isNotEmpty(itemRD) && itemRD.hasSucceeded) { this.item$.next(itemRD.payload); - this.duplicates$ = this.itemDataService.findDuplicates(itemRD.payload.uuid).pipe( + this.duplicates$ = this.duplicateDataService.findDuplicates(itemRD.payload.uuid).pipe( getFirstCompletedRemoteData(), map((remoteData: RemoteData>) => { if (remoteData.hasSucceeded) { diff --git a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts index e9e8088757c..151fd1fe564 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts @@ -25,6 +25,7 @@ import { Context } from '../../../../core/shared/context.model'; import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { Duplicate } from '../../duplicate-data/duplicate.model'; import { ItemDataService } from '../../../../core/data/item-data.service'; +import { DuplicateDataService } from '../../../../core/data/duplicate-search.service'; /** * This component renders pool task object for the search result in the list view. @@ -79,6 +80,7 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen public dsoNameService: DSONameService, protected objectCache: ObjectCacheService, protected itemDataService: ItemDataService, + protected duplicateDataService: DuplicateDataService, @Inject(APP_CONFIG) protected appConfig: AppConfig ) { super(truncatableService, dsoNameService, appConfig); @@ -109,7 +111,8 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen tap((itemRD: RemoteData) => { if (isNotEmpty(itemRD) && itemRD.hasSucceeded) { this.item$.next(itemRD.payload); - this.duplicates$ = this.itemDataService.findDuplicates(itemRD.payload.uuid).pipe( + // Find duplicates for this item + this.duplicates$ = this.duplicateDataService.findDuplicates(itemRD.payload.uuid).pipe( getFirstCompletedRemoteData(), map((remoteData: RemoteData>) => { if (remoteData.hasSucceeded) { @@ -120,7 +123,7 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen }) ); } - }) + }), ).subscribe(); this.showThumbnails = this.appConfig.browseBy.showThumbnails; From b79dab3fcb1dd64c5aad729e8dee11abc24d166f Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 27 Feb 2024 13:22:28 +1300 Subject: [PATCH 16/23] [TLC-674] Mock duplicateDataService providers in spec tests --- .../claimed-search-result-list-element.component.spec.ts | 8 ++++++-- .../pool-search-result-list-element.component.spec.ts | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts index 6c4616ea2e8..4d8cd225d9e 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts @@ -15,7 +15,7 @@ import { Item } from '../../../../core/shared/item.model'; import { ClaimedSearchResultListElementComponent } from './claimed-search-result-list-element.component'; import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model'; import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model'; -import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils'; +import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model'; import { TruncatableService } from '../../../truncatable/truncatable.service'; import { VarDirective } from '../../../utils/var.directive'; @@ -30,6 +30,7 @@ import { ObjectCacheService } from '../../../../core/cache/object-cache.service' import { Context } from '../../../../core/shared/context.model'; import { createPaginatedList } from '../../../testing/utils.test'; import { ItemDataService } from '../../../../core/data/item-data.service'; +import { DuplicateDataService } from '../../../../core/data/duplicate-search.service'; let component: ClaimedSearchResultListElementComponent; let fixture: ComponentFixture; @@ -39,7 +40,9 @@ mockResultObject.hitHighlights = {}; const emptyList = createSuccessfulRemoteDataObject(createPaginatedList([])); const itemDataServiceStub = { - findDuplicates: () => createSuccessfulRemoteDataObject$({}), + findListByHref: () => observableOf(emptyList), +}; +const duplicateDataServiceStub = { findListByHref: () => observableOf(emptyList), }; @@ -93,6 +96,7 @@ describe('ClaimedSearchResultListElementComponent', () => { { provide: APP_CONFIG, useValue: environment }, { provide: ObjectCacheService, useValue: objectCacheServiceMock }, { provide: ItemDataService, useValue: itemDataServiceStub }, + { provide: DuplicateDataService, useValue: duplicateDataServiceStub }, ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(ClaimedSearchResultListElementComponent, { diff --git a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts index 02e375b37d6..f629222d19c 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts @@ -15,7 +15,7 @@ import { Item } from '../../../../core/shared/item.model'; import { PoolSearchResultListElementComponent } from './pool-search-result-list-element.component'; import { PoolTask } from '../../../../core/tasks/models/pool-task-object.model'; import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model'; -import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils'; +import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; import { PoolTaskSearchResult } from '../../../object-collection/shared/pool-task-search-result.model'; import { TruncatableService } from '../../../truncatable/truncatable.service'; import { VarDirective } from '../../../utils/var.directive'; @@ -29,6 +29,7 @@ import { ObjectCacheService } from '../../../../core/cache/object-cache.service' import { Context } from '../../../../core/shared/context.model'; import { createPaginatedList } from '../../../testing/utils.test'; import { ItemDataService } from '../../../../core/data/item-data.service'; +import { DuplicateDataService } from '../../../../core/data/duplicate-search.service'; let component: PoolSearchResultListElementComponent; let fixture: ComponentFixture; @@ -38,7 +39,9 @@ mockResultObject.hitHighlights = {}; const emptyList = createSuccessfulRemoteDataObject(createPaginatedList([])); const itemDataServiceStub = { - findDuplicates: () => createSuccessfulRemoteDataObject$({}), + findListByHref: () => observableOf(emptyList), +}; +const duplicateDataServiceStub = { findListByHref: () => observableOf(emptyList), }; @@ -100,6 +103,7 @@ describe('PoolSearchResultListElementComponent', () => { { provide: APP_CONFIG, useValue: environmentUseThumbs }, { provide: ObjectCacheService, useValue: objectCacheServiceMock }, { provide: ItemDataService, useValue: itemDataServiceStub }, + { provide: DuplicateDataService, useValue: duplicateDataServiceStub } ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(PoolSearchResultListElementComponent, { From 6229966f7a2037814cbc75317fa5fec84f70bc5b Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 27 Feb 2024 13:40:45 +1300 Subject: [PATCH 17/23] [TLC-674] Mock duplicateDataService.findDuplicates() --- .../claimed-search-result-list-element.component.spec.ts | 4 +++- .../pool-search-result-list-element.component.spec.ts | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts index 4d8cd225d9e..349ac88a011 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts @@ -15,7 +15,7 @@ import { Item } from '../../../../core/shared/item.model'; import { ClaimedSearchResultListElementComponent } from './claimed-search-result-list-element.component'; import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model'; import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model'; -import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils'; import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model'; import { TruncatableService } from '../../../truncatable/truncatable.service'; import { VarDirective } from '../../../utils/var.directive'; @@ -41,9 +41,11 @@ mockResultObject.hitHighlights = {}; const emptyList = createSuccessfulRemoteDataObject(createPaginatedList([])); const itemDataServiceStub = { findListByHref: () => observableOf(emptyList), + }; const duplicateDataServiceStub = { findListByHref: () => observableOf(emptyList), + findDuplicates: () => createSuccessfulRemoteDataObject$({}), }; const item = Object.assign(new Item(), { diff --git a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts index f629222d19c..73cf09eb776 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts @@ -15,7 +15,7 @@ import { Item } from '../../../../core/shared/item.model'; import { PoolSearchResultListElementComponent } from './pool-search-result-list-element.component'; import { PoolTask } from '../../../../core/tasks/models/pool-task-object.model'; import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model'; -import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils'; import { PoolTaskSearchResult } from '../../../object-collection/shared/pool-task-search-result.model'; import { TruncatableService } from '../../../truncatable/truncatable.service'; import { VarDirective } from '../../../utils/var.directive'; @@ -43,6 +43,7 @@ const itemDataServiceStub = { }; const duplicateDataServiceStub = { findListByHref: () => observableOf(emptyList), + findDuplicates: () => createSuccessfulRemoteDataObject$({}), }; const item = Object.assign(new Item(), { From e76b6c962cc6d68df13fdc8552a96cf4f1a85247 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 28 Feb 2024 12:19:08 +1300 Subject: [PATCH 18/23] [TLC-674] Duplicate detection frontend changes as per feedback --- src/app/core/core.module.ts | 4 +- src/app/core/data/item-data.service.ts | 19 ------- .../submission-duplicate-data.service.ts} | 28 ++++++---- .../duplicate-data/duplicate.model.ts | 21 ++++++- ...arch-result-list-element.component.spec.ts | 16 +++++- ...ed-search-result-list-element.component.ts | 48 ++++++++++++++-- ...-search-result-list-element.component.html | 1 + ...arch-result-list-element.component.spec.ts | 15 ++++- ...ol-search-result-list-element.component.ts | 56 +++++++++++++++---- .../section-duplicates.component.spec.ts | 2 +- 10 files changed, 154 insertions(+), 56 deletions(-) rename src/app/core/{data/duplicate-search.service.ts => submission/submission-duplicate-data.service.ts} (72%) diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 8a0edcf8022..48e48738bf4 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -198,7 +198,7 @@ import { NotifyRequestsStatus } from '../item-page/simple/notify-requests-status import { LdnService } from '../admin/admin-ldn-services/ldn-services-model/ldn-services.model'; import { Itemfilter } from '../admin/admin-ldn-services/ldn-services-model/ldn-service-itemfilters'; import { SubmissionCoarNotifyConfig } from '../submission/sections/section-coar-notify/submission-coar-notify.config'; -import { DuplicateDataService } from './data/duplicate-search.service'; +import { SubmissionDuplicateDataService } from './submission/submission-duplicate-data.service'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -235,7 +235,7 @@ const PROVIDERS = [ HALEndpointService, HostWindowService, ItemDataService, - DuplicateDataService, + SubmissionDuplicateDataService, MetadataService, ObjectCacheService, PaginationComponentOptions, diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 1a0d181ca1d..c3fa84dd6c8 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -46,9 +46,6 @@ import { RestRequestMethod } from './rest-request-method'; import { CreateData, CreateDataImpl } from './base/create-data'; import { RequestParam } from '../cache/models/request-param.model'; import { dataService } from './base/data-service.decorator'; -import { Duplicate } from '../../shared/object-list/duplicate-data/duplicate.model'; -import { SearchDataImpl } from './base/search-data'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; /** * An abstract service for CRUD operations on Items @@ -59,7 +56,6 @@ export abstract class BaseItemDataService extends IdentifiableDataService private createData: CreateData; private patchData: PatchData; private deleteData: DeleteData; - private searchData: SearchDataImpl; protected constructor( protected linkPath, @@ -78,7 +74,6 @@ export abstract class BaseItemDataService extends IdentifiableDataService this.createData = new CreateDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive); this.patchData = new PatchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, comparator, this.responseMsToLive, this.constructIdEndpoint); this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint); - this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); } /** @@ -247,20 +242,6 @@ export abstract class BaseItemDataService extends IdentifiableDataService ); } - public findDuplicates(uuid: string, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { - const searchParams = [new RequestParam('uuid', uuid)]; - let findListOptions = new FindListOptions(); - if (options) { - findListOptions = Object.assign(new FindListOptions(), options); - } - if (findListOptions.searchParams) { - findListOptions.searchParams = [...findListOptions.searchParams, ...searchParams]; - } else { - findListOptions.searchParams = searchParams; - } - return this.searchData.searchBy('findDuplicates', findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); - } - /** * Get the endpoint to move the item * @param itemId diff --git a/src/app/core/data/duplicate-search.service.ts b/src/app/core/submission/submission-duplicate-data.service.ts similarity index 72% rename from src/app/core/data/duplicate-search.service.ts rename to src/app/core/submission/submission-duplicate-data.service.ts index f33188119af..5410e51332a 100644 --- a/src/app/core/data/duplicate-search.service.ts +++ b/src/app/core/submission/submission-duplicate-data.service.ts @@ -3,28 +3,34 @@ import { Observable } from 'rxjs'; import { Injectable } from '@angular/core'; import { map } from 'rxjs/operators'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { ResponseParsingService } from './parsing.service'; -import { RemoteData } from './remote-data'; -import { GetRequest } from './request.models'; -import { RequestService } from './request.service'; +import { ResponseParsingService } from '../data/parsing.service'; +import { RemoteData } from '../data/remote-data'; +import { GetRequest } from '../data/request.models'; +import { RequestService } from '../data/request.service'; import { GenericConstructor } from '../shared/generic-constructor'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { SearchResponseParsingService } from './search-response-parsing.service'; +import { SearchResponseParsingService } from '../data/search-response-parsing.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { RestRequest } from './rest-request.model'; -import { BaseDataService } from './base/base-data.service'; -import { FindListOptions } from './find-list-options.model'; +import { RestRequest } from '../data/rest-request.model'; +import { BaseDataService } from '../data/base/base-data.service'; +import { FindListOptions } from '../data/find-list-options.model'; import { Duplicate } from '../../shared/object-list/duplicate-data/duplicate.model'; -import { PaginatedList } from './paginated-list.model'; +import { PaginatedList } from '../data/paginated-list.model'; import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; /** - * Service that performs all general actions that have to do with the search page + * Service that handles search requests for potential duplicate items. + * This uses the /api/submission/duplicates endpoint to look for other archived or in-progress items (if user + * has READ permission) that match the item (for the given uuid). + * Matching is configured in the backend in dspace/config/modulesduplicate-detection.cfg + * The returned results are small preview 'stubs' of items, and displayed in either a submission section + * or the workflow pooled/claimed task page. + * */ @Injectable() -export class DuplicateDataService extends BaseDataService { +export class SubmissionDuplicateDataService extends BaseDataService { /** * The ResponseParsingService constructor name diff --git a/src/app/shared/object-list/duplicate-data/duplicate.model.ts b/src/app/shared/object-list/duplicate-data/duplicate.model.ts index cbcff155e10..a165b81bab4 100644 --- a/src/app/shared/object-list/duplicate-data/duplicate.model.ts +++ b/src/app/shared/object-list/duplicate-data/duplicate.model.ts @@ -5,6 +5,10 @@ import { CacheableObject } from '../../../core/cache/cacheable-object.model'; import { DUPLICATE } from './duplicate.resource-type'; import { ResourceType } from '../../../core/shared/resource-type'; +/** + * This implements the model of a duplicate preview stub, to be displayed to submitters or reviewers + * if duplicate detection is enabled. The metadata map is configurable in the backend at duplicate-detection.cfg + */ export class Duplicate implements CacheableObject { static type = DUPLICATE; @@ -14,17 +18,28 @@ export class Duplicate implements CacheableObject { */ @autoserialize title: string; + /** + * The item uuid + */ @autoserialize uuid: string; + /** + * The workfow item ID, if any + */ @autoserialize workflowItemId: number; + /** + * The workspace item ID, if any + */ @autoserialize workspaceItemId: number; + /** + * The owning collection of the item + */ @autoserialize owningCollection: string; - /** - * Metadata for the bitstream (e.g. dc.description) + * Metadata for the preview item (e.g. dc.title) */ @autoserialize metadata: MetadataMap; @@ -33,7 +48,7 @@ export class Duplicate implements CacheableObject { type: ResourceType; /** - * The {@link HALLink}s for this Bitstream + * The {@link HALLink}s for the URL that generated this item (in context of search results) */ @deserialize _links: { diff --git a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts index 349ac88a011..7e513319a15 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts @@ -30,7 +30,9 @@ import { ObjectCacheService } from '../../../../core/cache/object-cache.service' import { Context } from '../../../../core/shared/context.model'; import { createPaginatedList } from '../../../testing/utils.test'; import { ItemDataService } from '../../../../core/data/item-data.service'; -import { DuplicateDataService } from '../../../../core/data/duplicate-search.service'; +import { SubmissionDuplicateDataService } from '../../../../core/submission/submission-duplicate-data.service'; +import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model'; +import { ConfigurationDataService } from '../../../../core/data/configuration-data.service'; let component: ClaimedSearchResultListElementComponent; let fixture: ComponentFixture; @@ -41,8 +43,15 @@ mockResultObject.hitHighlights = {}; const emptyList = createSuccessfulRemoteDataObject(createPaginatedList([])); const itemDataServiceStub = { findListByHref: () => observableOf(emptyList), - }; +const configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'duplicate.enable', + values: [ + 'true' + ] + })) +}); const duplicateDataServiceStub = { findListByHref: () => observableOf(emptyList), findDuplicates: () => createSuccessfulRemoteDataObject$({}), @@ -98,7 +107,8 @@ describe('ClaimedSearchResultListElementComponent', () => { { provide: APP_CONFIG, useValue: environment }, { provide: ObjectCacheService, useValue: objectCacheServiceMock }, { provide: ItemDataService, useValue: itemDataServiceStub }, - { provide: DuplicateDataService, useValue: duplicateDataServiceStub }, + { provide: ConfigurationDataService, useValue: configurationDataService }, + { provide: SubmissionDuplicateDataService, useValue: duplicateDataServiceStub }, ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(ClaimedSearchResultListElementComponent, { 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 92adbc28ca6..b4a61a6290e 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 @@ -5,7 +5,7 @@ import { listableObjectComponent } from '../../../object-collection/shared/lista import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model'; import { LinkService } from '../../../../core/cache/builders/link.service'; import { TruncatableService } from '../../../truncatable/truncatable.service'; -import { BehaviorSubject, EMPTY, Observable } from 'rxjs'; +import {BehaviorSubject, combineLatest, EMPTY, Observable} from 'rxjs'; import { RemoteData } from '../../../../core/data/remote-data'; import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model'; import { followLink } from '../../../utils/follow-link-config.model'; @@ -24,7 +24,9 @@ import { Context } from '../../../../core/shared/context.model'; import { Duplicate } from '../../duplicate-data/duplicate.model'; import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { ItemDataService } from '../../../../core/data/item-data.service'; -import { DuplicateDataService } from '../../../../core/data/duplicate-search.service'; +import { SubmissionDuplicateDataService } from '../../../../core/submission/submission-duplicate-data.service'; +import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model'; +import { ConfigurationDataService } from '../../../../core/data/configuration-data.service'; @Component({ selector: 'ds-claimed-search-result-list-element', @@ -57,7 +59,7 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle /** * The potential duplicates of this item */ - public duplicates$: Observable = new Observable(); + public duplicates$: Observable; /** * Display thumbnails if required by configuration @@ -70,7 +72,8 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle public dsoNameService: DSONameService, protected objectCache: ObjectCacheService, protected itemDataService: ItemDataService, - protected duplicateDataService: DuplicateDataService, + protected configService: ConfigurationDataService, + protected duplicateDataService: SubmissionDuplicateDataService, @Inject(APP_CONFIG) protected appConfig: AppConfig ) { super(truncatableService, dsoNameService, appConfig); @@ -114,10 +117,45 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle } }) ).subscribe(); - + // Initialise duplicates, if enabled + this.duplicates$ = this.initializeDuplicateDetectionIfEnabled(); this.showThumbnails = this.appConfig.browseBy.showThumbnails; } + /** + * Initialize and set the duplicates observable based on whether the configuration in REST is enabled + * and the results returned + */ + initializeDuplicateDetectionIfEnabled() { + return combineLatest([ + this.configService.findByPropertyName('duplicate.enable').pipe( + getFirstCompletedRemoteData(), + map((remoteData: RemoteData) => { + return (remoteData.isSuccess && remoteData.payload && remoteData.payload.values[0] === 'true'); + }) + ), + this.item$.pipe(), + ] + ).pipe( + map(([enabled, rd]) => { + if (enabled) { + this.duplicates$ = this.duplicateDataService.findDuplicates(rd.uuid).pipe( + getFirstCompletedRemoteData(), + map((remoteData: RemoteData>) => { + if (remoteData.hasSucceeded) { + if (remoteData.payload.page) { + return remoteData.payload.page; + } + } + }) + ); + } else { + return [] as Duplicate[]; + } + }), + ); + } + ngOnDestroy() { // This ensures the object is removed from cache, when action is performed on task if (hasValue(this.dso)) { 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 a2b73eb761c..ab7d7a7d8a3 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,7 @@ [showSubmitter]="showSubmitter" [badgeContext]="badgeContext" [workflowItem]="workflowitem$.value"> +
diff --git a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts index 73cf09eb776..555449a3a4e 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts @@ -29,7 +29,9 @@ import { ObjectCacheService } from '../../../../core/cache/object-cache.service' import { Context } from '../../../../core/shared/context.model'; import { createPaginatedList } from '../../../testing/utils.test'; import { ItemDataService } from '../../../../core/data/item-data.service'; -import { DuplicateDataService } from '../../../../core/data/duplicate-search.service'; +import { SubmissionDuplicateDataService } from '../../../../core/submission/submission-duplicate-data.service'; +import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model'; +import { ConfigurationDataService } from '../../../../core/data/configuration-data.service'; let component: PoolSearchResultListElementComponent; let fixture: ComponentFixture; @@ -41,6 +43,14 @@ const emptyList = createSuccessfulRemoteDataObject(createPaginatedList([])); const itemDataServiceStub = { findListByHref: () => observableOf(emptyList), }; +const configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'duplicate.enable', + values: [ + 'true' + ] + })) +}); const duplicateDataServiceStub = { findListByHref: () => observableOf(emptyList), findDuplicates: () => createSuccessfulRemoteDataObject$({}), @@ -104,7 +114,8 @@ describe('PoolSearchResultListElementComponent', () => { { provide: APP_CONFIG, useValue: environmentUseThumbs }, { provide: ObjectCacheService, useValue: objectCacheServiceMock }, { provide: ItemDataService, useValue: itemDataServiceStub }, - { provide: DuplicateDataService, useValue: duplicateDataServiceStub } + { provide: ConfigurationDataService, useValue: configurationDataService }, + { provide: SubmissionDuplicateDataService, useValue: duplicateDataServiceStub } ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(PoolSearchResultListElementComponent, { diff --git a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts index 151fd1fe564..5800d58d1d1 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts @@ -1,6 +1,6 @@ import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; -import { BehaviorSubject, EMPTY, Observable } from 'rxjs'; +import {BehaviorSubject, combineLatest, EMPTY, Observable } from 'rxjs'; import { map, mergeMap, tap } from 'rxjs/operators'; import { ViewMode } from '../../../../core/shared/view-mode.model'; @@ -25,7 +25,9 @@ import { Context } from '../../../../core/shared/context.model'; import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { Duplicate } from '../../duplicate-data/duplicate.model'; import { ItemDataService } from '../../../../core/data/item-data.service'; -import { DuplicateDataService } from '../../../../core/data/duplicate-search.service'; +import { SubmissionDuplicateDataService } from '../../../../core/submission/submission-duplicate-data.service'; +import { ConfigurationDataService } from '../../../../core/data/configuration-data.service'; +import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model'; /** * This component renders pool task object for the search result in the list view. @@ -62,7 +64,7 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen /** * The potential duplicates of this workflow item */ - public duplicates$: Observable = new Observable(); + public duplicates$: Observable; /** * The index of this list element @@ -74,13 +76,16 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen */ showThumbnails: boolean; + enableDetectDuplicates$: Observable; + constructor( protected linkService: LinkService, protected truncatableService: TruncatableService, public dsoNameService: DSONameService, protected objectCache: ObjectCacheService, protected itemDataService: ItemDataService, - protected duplicateDataService: DuplicateDataService, + protected configService: ConfigurationDataService, + protected duplicateDataService: SubmissionDuplicateDataService, @Inject(APP_CONFIG) protected appConfig: AppConfig ) { super(truncatableService, dsoNameService, appConfig); @@ -96,6 +101,14 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen followLink('submitter') ), followLink('action')); + // Get configuration for duplicate detection feature + this.enableDetectDuplicates$ = this.configService.findByPropertyName('duplicate.enable').pipe( + getFirstCompletedRemoteData(), + map((rd: RemoteData) => { + return (rd.hasSucceeded && rd.payload && rd.payload.values[0] === 'true'); + }) + ); + (this.dso.workflowitem as Observable>).pipe( getFirstCompletedRemoteData(), mergeMap((wfiRD: RemoteData) => { @@ -111,8 +124,32 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen tap((itemRD: RemoteData) => { if (isNotEmpty(itemRD) && itemRD.hasSucceeded) { this.item$.next(itemRD.payload); - // Find duplicates for this item - this.duplicates$ = this.duplicateDataService.findDuplicates(itemRD.payload.uuid).pipe( + } + }), + ).subscribe(); + this.showThumbnails = this.appConfig.browseBy.showThumbnails; + // Initialise duplicates, if enabled + this.duplicates$ = this.initializeDuplicateDetectionIfEnabled(); + } + + /** + * Initialize and set the duplicates observable based on whether the configuration in REST is enabled + * and the results returned + */ + initializeDuplicateDetectionIfEnabled() { + return combineLatest([ + this.configService.findByPropertyName('duplicate.enable').pipe( + getFirstCompletedRemoteData(), + map((remoteData: RemoteData) => { + return (remoteData.isSuccess && remoteData.payload && remoteData.payload.values[0] === 'true'); + }) + ), + this.item$.pipe(), + ] + ).pipe( + map(([enabled, rd]) => { + if (enabled) { + this.duplicates$ = this.duplicateDataService.findDuplicates(rd.uuid).pipe( getFirstCompletedRemoteData(), map((remoteData: RemoteData>) => { if (remoteData.hasSucceeded) { @@ -122,12 +159,11 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen } }) ); + } else { + return [] as Duplicate[]; } }), - ).subscribe(); - - this.showThumbnails = this.appConfig.browseBy.showThumbnails; - + ); } ngOnDestroy() { diff --git a/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts b/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts index 5d00db41596..2c581fee97e 100644 --- a/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts +++ b/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts @@ -76,7 +76,7 @@ const duplicates: Duplicate[] = [{ type: DUPLICATE, _links: { self: { - href: 'http://localhost:8080/server/api/core/items/search/findDuplicates?uuid=testid' + href: 'http://localhost:8080/server/api/core/submission/duplicates/search?uuid=testid' } } }]; From 1e36a10dd53d7ea0143d9e4ea28ff6e8a096b512 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Thu, 29 Feb 2024 11:06:03 +1300 Subject: [PATCH 19/23] [TLC-674] Properly test for existence of duplicates section --- .../pool-search-result-list-element.component.ts | 10 ---------- .../submission/objects/submission-objects.effects.ts | 2 +- .../submission/objects/submission-objects.reducer.ts | 2 +- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts index 5800d58d1d1..3ada585f00f 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts @@ -76,8 +76,6 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen */ showThumbnails: boolean; - enableDetectDuplicates$: Observable; - constructor( protected linkService: LinkService, protected truncatableService: TruncatableService, @@ -101,14 +99,6 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen followLink('submitter') ), followLink('action')); - // Get configuration for duplicate detection feature - this.enableDetectDuplicates$ = this.configService.findByPropertyName('duplicate.enable').pipe( - getFirstCompletedRemoteData(), - map((rd: RemoteData) => { - return (rd.hasSucceeded && rd.payload && rd.payload.values[0] === 'true'); - }) - ); - (this.dso.workflowitem as Observable>).pipe( getFirstCompletedRemoteData(), mergeMap((wfiRD: RemoteData) => { diff --git a/src/app/submission/objects/submission-objects.effects.ts b/src/app/submission/objects/submission-objects.effects.ts index c4fd42b0d32..8fd7fba5012 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -449,7 +449,7 @@ export class SubmissionObjectEffects { // or if configuration overrides this behaviour if (!alwaysDisplayDuplicates()) { const duplicatesSectionId = findKey(currentState.sections, (section) => section.sectionType === SectionsType.Duplicates); - if (isNotUndefined(duplicatesSectionId) && isEmpty((sections[duplicatesSectionId] as WorkspaceitemSectionDuplicatesObject).potentialDuplicates)) { + if (isNotUndefined(duplicatesSectionId) && sections.hasOwnProperty(duplicatesSectionId) && isEmpty((sections[duplicatesSectionId] as WorkspaceitemSectionDuplicatesObject).potentialDuplicates)) { mappedActions.push(new CleanDuplicateDetectionAction(submissionId)); } } diff --git a/src/app/submission/objects/submission-objects.reducer.ts b/src/app/submission/objects/submission-objects.reducer.ts index c81f98db6a0..268329460ee 100644 --- a/src/app/submission/objects/submission-objects.reducer.ts +++ b/src/app/submission/objects/submission-objects.reducer.ts @@ -862,7 +862,7 @@ function deleteFile(state: SubmissionObjectState, action: DeleteUploadedFileActi } function cleanDuplicateDetectionSection(state: SubmissionObjectState, action: CleanDuplicateDetectionAction): SubmissionObjectState { - if (isNotEmpty(state[ action.payload.submissionId ])) { + if (isNotEmpty(state[ action.payload.submissionId ]) && state[action.payload.submissionId].sections.hasOwnProperty('duplicates')) { return Object.assign({}, state, { [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { sections: Object.assign({}, state[ action.payload.submissionId ].sections, { From 1206d614402946b43392e4dff9e8bb13364dbbdc Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Mon, 4 Mar 2024 11:42:08 +1300 Subject: [PATCH 20/23] [TLC-674] Update duplicate data service to use searchBy method --- .../submission-duplicate-data.service.ts | 48 +++++++++++++++---- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/src/app/core/submission/submission-duplicate-data.service.ts b/src/app/core/submission/submission-duplicate-data.service.ts index 5410e51332a..bb440722dcb 100644 --- a/src/app/core/submission/submission-duplicate-data.service.ts +++ b/src/app/core/submission/submission-duplicate-data.service.ts @@ -1,7 +1,6 @@ /* eslint-disable max-classes-per-file */ import { Observable } from 'rxjs'; import { Injectable } from '@angular/core'; -import { map } from 'rxjs/operators'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { ResponseParsingService } from '../data/parsing.service'; import { RemoteData } from '../data/remote-data'; @@ -18,6 +17,7 @@ import { Duplicate } from '../../shared/object-list/duplicate-data/duplicate.mod import { PaginatedList } from '../data/paginated-list.model'; import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { SearchData, SearchDataImpl } from '../data/base/search-data'; /** @@ -30,7 +30,7 @@ import { ObjectCacheService } from '../cache/object-cache.service'; * */ @Injectable() -export class SubmissionDuplicateDataService extends BaseDataService { +export class SubmissionDuplicateDataService extends BaseDataService implements SearchData { /** * The ResponseParsingService constructor name @@ -42,6 +42,12 @@ export class SubmissionDuplicateDataService extends BaseDataService { */ private request: GenericConstructor = GetRequest; + /** + * SearchData interface to implement + * @private + */ + private searchData: SearchData; + /** * Subscription to unsubscribe from */ @@ -54,8 +60,26 @@ export class SubmissionDuplicateDataService extends BaseDataService { protected halService: HALEndpointService, ) { super('duplicates', requestService, rdbService, objectCache, halService); + this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); + } + + /** + * Implement the searchBy method to return paginated lists of Duplicate resources + * + * @param searchMethod the search method name + * @param options find list options + * @param useCachedVersionIfAvailable whether to use cached version if available + * @param reRequestOnStale whether to rerequest results on stale + * @param linksToFollow links to follow in results + */ + searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } + /** + * Helper method to get the duplicates endpoint + * @protected + */ protected getEndpoint(): Observable { return this.halService.getEndpoint(this.linkPath); } @@ -74,13 +98,16 @@ export class SubmissionDuplicateDataService extends BaseDataService { } } - private getSearchUrl(): Observable { - const href$ = this.getEndpoint(); - return href$.pipe( - map((href) => href + '/search') - ); - } - + /** + * Find duplicates for a given item UUID. Locates and returns results from the /api/submission/duplicates/search/findByItem + * SearchRestMethod, which is why this implements SearchData and searchBy + * + * @param uuid the item UUID + * @param options any find list options e.g. paging + * @param useCachedVersionIfAvailable whether to use cached version if available + * @param reRequestOnStale whether to rerequest results on stale + * @param linksToFollow links to follow in results + */ public findDuplicates(uuid: string, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { const searchParams = [new RequestParam('uuid', uuid)]; let findListOptions = new FindListOptions(); @@ -93,7 +120,8 @@ export class SubmissionDuplicateDataService extends BaseDataService { findListOptions.searchParams = searchParams; } - return this.findListByHref(this.getSearchUrl(), findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + // Perform the actual search by search + return this.searchBy('findByItem', findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } /** From 2000b1b2408cd60756397d8f1a3c8af353346cba Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Mon, 4 Mar 2024 12:00:51 +1300 Subject: [PATCH 21/23] [TLC-674] Resolve sections type list problem (introduced in conflict merge) --- src/app/submission/sections/sections-type.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/submission/sections/sections-type.ts b/src/app/submission/sections/sections-type.ts index f06e3bc8fc8..50d15427d2b 100644 --- a/src/app/submission/sections/sections-type.ts +++ b/src/app/submission/sections/sections-type.ts @@ -9,6 +9,6 @@ export enum SectionsType { SherpaPolicies = 'sherpaPolicy', Identifiers = 'identifiers', Collection = 'collection', - CoarNotify = 'coarnotify' + CoarNotify = 'coarnotify', Duplicates = 'duplicates' } From ca32314951993abf5922e30f84df48622a86251c Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Mon, 4 Mar 2024 13:31:48 +1300 Subject: [PATCH 22/23] [TLC-674] Fix findDuplicates error in claimed task (old code) Update spec tests --- .../submission-duplicate-data.service.ts | 6 +++++- ...ed-search-result-list-element.component.spec.ts | 6 +----- ...claimed-search-result-list-element.component.ts | 14 +------------- ...ol-search-result-list-element.component.spec.ts | 6 +----- .../pool-search-result-list-element.component.ts | 2 -- 5 files changed, 8 insertions(+), 26 deletions(-) diff --git a/src/app/core/submission/submission-duplicate-data.service.ts b/src/app/core/submission/submission-duplicate-data.service.ts index bb440722dcb..7e0e97e80b8 100644 --- a/src/app/core/submission/submission-duplicate-data.service.ts +++ b/src/app/core/submission/submission-duplicate-data.service.ts @@ -18,6 +18,8 @@ import { PaginatedList } from '../data/paginated-list.model'; import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { SearchData, SearchDataImpl } from '../data/base/search-data'; +import { DUPLICATE } from '../../shared/object-list/duplicate-data/duplicate.resource-type'; +import { dataService } from '../data/base/data-service.decorator'; /** @@ -30,6 +32,7 @@ import { SearchData, SearchDataImpl } from '../data/base/search-data'; * */ @Injectable() +@dataService(DUPLICATE) export class SubmissionDuplicateDataService extends BaseDataService implements SearchData { /** @@ -120,8 +123,9 @@ export class SubmissionDuplicateDataService extends BaseDataService i findListOptions.searchParams = searchParams; } - // Perform the actual search by search + // Return actual search/findByItem results return this.searchBy('findByItem', findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } /** diff --git a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts index 7e513319a15..d0184465de9 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts @@ -29,7 +29,6 @@ import { environment } from '../../../../../environments/environment'; import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; import { Context } from '../../../../core/shared/context.model'; import { createPaginatedList } from '../../../testing/utils.test'; -import { ItemDataService } from '../../../../core/data/item-data.service'; import { SubmissionDuplicateDataService } from '../../../../core/submission/submission-duplicate-data.service'; import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model'; import { ConfigurationDataService } from '../../../../core/data/configuration-data.service'; @@ -41,9 +40,7 @@ const mockResultObject: ClaimedTaskSearchResult = new ClaimedTaskSearchResult(); mockResultObject.hitHighlights = {}; const emptyList = createSuccessfulRemoteDataObject(createPaginatedList([])); -const itemDataServiceStub = { - findListByHref: () => observableOf(emptyList), -}; + const configurationDataService = jasmine.createSpyObj('configurationDataService', { findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { name: 'duplicate.enable', @@ -106,7 +103,6 @@ describe('ClaimedSearchResultListElementComponent', () => { { provide: DSONameService, useClass: DSONameServiceMock }, { provide: APP_CONFIG, useValue: environment }, { provide: ObjectCacheService, useValue: objectCacheServiceMock }, - { provide: ItemDataService, useValue: itemDataServiceStub }, { provide: ConfigurationDataService, useValue: configurationDataService }, { provide: SubmissionDuplicateDataService, useValue: duplicateDataServiceStub }, ], 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 b4a61a6290e..2dd87ec1a1a 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 @@ -23,7 +23,6 @@ import { isNotEmpty, hasValue } from '../../../empty.util'; import { Context } from '../../../../core/shared/context.model'; import { Duplicate } from '../../duplicate-data/duplicate.model'; import { PaginatedList } from '../../../../core/data/paginated-list.model'; -import { ItemDataService } from '../../../../core/data/item-data.service'; import { SubmissionDuplicateDataService } from '../../../../core/submission/submission-duplicate-data.service'; import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model'; import { ConfigurationDataService } from '../../../../core/data/configuration-data.service'; @@ -71,7 +70,6 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle protected truncatableService: TruncatableService, public dsoNameService: DSONameService, protected objectCache: ObjectCacheService, - protected itemDataService: ItemDataService, protected configService: ConfigurationDataService, protected duplicateDataService: SubmissionDuplicateDataService, @Inject(APP_CONFIG) protected appConfig: AppConfig @@ -104,22 +102,12 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle tap((itemRD: RemoteData) => { if (isNotEmpty(itemRD) && itemRD.hasSucceeded) { this.item$.next(itemRD.payload); - this.duplicates$ = this.duplicateDataService.findDuplicates(itemRD.payload.uuid).pipe( - getFirstCompletedRemoteData(), - map((remoteData: RemoteData>) => { - if (remoteData.hasSucceeded) { - if (remoteData.payload.page) { - return remoteData.payload.page; - } - } - }) - ); } }) ).subscribe(); + this.showThumbnails = this.appConfig.browseBy.showThumbnails; // Initialise duplicates, if enabled this.duplicates$ = this.initializeDuplicateDetectionIfEnabled(); - this.showThumbnails = this.appConfig.browseBy.showThumbnails; } /** diff --git a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts index 555449a3a4e..47c08fbe949 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts @@ -28,7 +28,6 @@ import { APP_CONFIG } from '../../../../../config/app-config.interface'; import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; import { Context } from '../../../../core/shared/context.model'; import { createPaginatedList } from '../../../testing/utils.test'; -import { ItemDataService } from '../../../../core/data/item-data.service'; import { SubmissionDuplicateDataService } from '../../../../core/submission/submission-duplicate-data.service'; import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model'; import { ConfigurationDataService } from '../../../../core/data/configuration-data.service'; @@ -40,9 +39,7 @@ const mockResultObject: PoolTaskSearchResult = new PoolTaskSearchResult(); mockResultObject.hitHighlights = {}; const emptyList = createSuccessfulRemoteDataObject(createPaginatedList([])); -const itemDataServiceStub = { - findListByHref: () => observableOf(emptyList), -}; + const configurationDataService = jasmine.createSpyObj('configurationDataService', { findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { name: 'duplicate.enable', @@ -113,7 +110,6 @@ describe('PoolSearchResultListElementComponent', () => { { provide: DSONameService, useClass: DSONameServiceMock }, { provide: APP_CONFIG, useValue: environmentUseThumbs }, { provide: ObjectCacheService, useValue: objectCacheServiceMock }, - { provide: ItemDataService, useValue: itemDataServiceStub }, { provide: ConfigurationDataService, useValue: configurationDataService }, { provide: SubmissionDuplicateDataService, useValue: duplicateDataServiceStub } ], diff --git a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts index 3ada585f00f..087f234fd36 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts @@ -24,7 +24,6 @@ import { isNotEmpty, hasValue } from '../../../empty.util'; import { Context } from '../../../../core/shared/context.model'; import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { Duplicate } from '../../duplicate-data/duplicate.model'; -import { ItemDataService } from '../../../../core/data/item-data.service'; import { SubmissionDuplicateDataService } from '../../../../core/submission/submission-duplicate-data.service'; import { ConfigurationDataService } from '../../../../core/data/configuration-data.service'; import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model'; @@ -81,7 +80,6 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen protected truncatableService: TruncatableService, public dsoNameService: DSONameService, protected objectCache: ObjectCacheService, - protected itemDataService: ItemDataService, protected configService: ConfigurationDataService, protected duplicateDataService: SubmissionDuplicateDataService, @Inject(APP_CONFIG) protected appConfig: AppConfig From e4a91e72610dad96c46b00e3cc34b4d13d2522d1 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Mon, 4 Mar 2024 14:29:26 +1300 Subject: [PATCH 23/23] [TLC-674] New duplicate data service, object reducer tests --- .../submission-duplicate-data.service.spec.ts | 30 +++++++++++++++++++ src/app/shared/mocks/submission.mock.ts | 5 +++- .../objects/submission-objects.actions.ts | 1 + .../submission-objects.reducer.spec.ts | 20 +++++++++++-- 4 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 src/app/core/submission/submission-duplicate-data.service.spec.ts diff --git a/src/app/core/submission/submission-duplicate-data.service.spec.ts b/src/app/core/submission/submission-duplicate-data.service.spec.ts new file mode 100644 index 00000000000..fff4f3a0bc5 --- /dev/null +++ b/src/app/core/submission/submission-duplicate-data.service.spec.ts @@ -0,0 +1,30 @@ +import { SubmissionDuplicateDataService } from './submission-duplicate-data.service'; +import { FindListOptions } from '../data/find-list-options.model'; +import { RequestParam } from '../cache/models/request-param.model'; + +/** + * Basic tests for the submission-duplicate-data.service.ts service + */ +describe('SubmissionDuplicateDataService', () => { + const duplicateDataService = new SubmissionDuplicateDataService(null, null, null, null); + + // Test the findDuplicates method to make sure that a call results in an expected + // call to searchBy, using the 'findByItem' search method + describe('findDuplicates', () => { + beforeEach(() => { + spyOn(duplicateDataService, 'searchBy'); + }); + + it('should call searchBy with the correct arguments', () => { + // Set up expected search parameters and find options + const searchParams = []; + searchParams.push(new RequestParam('uuid', 'test')); + let findListOptions = new FindListOptions(); + findListOptions.searchParams = searchParams; + // Perform test search using uuid 'test' using the findDuplicates method + const result = duplicateDataService.findDuplicates('test', new FindListOptions(), true, true); + // Expect searchBy('findByItem'...) to have been used as SearchData impl with the expected options (uuid=test) + expect(duplicateDataService.searchBy).toHaveBeenCalledWith('findByItem', findListOptions, true, true); + }); + }); +}); diff --git a/src/app/shared/mocks/submission.mock.ts b/src/app/shared/mocks/submission.mock.ts index bec08013c35..385df9dff61 100644 --- a/src/app/shared/mocks/submission.mock.ts +++ b/src/app/shared/mocks/submission.mock.ts @@ -1114,7 +1114,10 @@ export const mockSubmissionState: SubmissionObjectState = Object.assign({}, { isLoading: false, isValid: false, removePending: false - } as any + } as any, + 'duplicates': { + potentialDuplicates: [] + } as any, }, isLoading: false, savePending: false, diff --git a/src/app/submission/objects/submission-objects.actions.ts b/src/app/submission/objects/submission-objects.actions.ts index 24d9378f353..91309180b90 100644 --- a/src/app/submission/objects/submission-objects.actions.ts +++ b/src/app/submission/objects/submission-objects.actions.ts @@ -867,6 +867,7 @@ export type SubmissionObjectAction = DisableSectionAction | InitSubmissionFormAction | ResetSubmissionFormAction | CancelSubmissionFormAction + | CleanDuplicateDetectionAction | CompleteInitSubmissionFormAction | ChangeSubmissionCollectionAction | SaveAndDepositSubmissionAction diff --git a/src/app/submission/objects/submission-objects.reducer.spec.ts b/src/app/submission/objects/submission-objects.reducer.spec.ts index 2a24afae19c..35eb281d608 100644 --- a/src/app/submission/objects/submission-objects.reducer.spec.ts +++ b/src/app/submission/objects/submission-objects.reducer.spec.ts @@ -1,7 +1,7 @@ import { submissionObjectReducer, SubmissionObjectState } from './submission-objects.reducer'; import { CancelSubmissionFormAction, - ChangeSubmissionCollectionAction, + ChangeSubmissionCollectionAction, CleanDuplicateDetectionAction, CompleteInitSubmissionFormAction, DeleteSectionErrorsAction, DeleteUploadedFileAction, @@ -273,7 +273,7 @@ describe('submissionReducer test suite', () => { expect(newState[826].sections.traditionalpagetwo.enabled).toBeTruthy(); }); - it('should enable submission section properly', () => { + it('should disable submission section properly', () => { let action: SubmissionObjectAction = new EnableSectionAction(submissionId, 'traditionalpagetwo'); let newState = submissionObjectReducer(initState, action); @@ -644,4 +644,20 @@ describe('submissionReducer test suite', () => { expect(newState[826].sections.upload.data).toEqual(expectedState); }); + it('should enable duplicates section properly', () => { + + let action: SubmissionObjectAction = new EnableSectionAction(submissionId, 'duplicates'); + let newState = submissionObjectReducer(initState, action); + + expect(newState[826].sections.duplicates.enabled).toBeTruthy(); + }); + + it('should clean duplicates section properly', () => { + + let action = new CleanDuplicateDetectionAction(submissionId); + let newState = submissionObjectReducer(initState, action); + + expect(newState[826].sections.duplicates.enabled).toBeFalsy(); + }); + });