Skip to content

Commit

Permalink
[TLC-674] Duplicate detection comp, template, i18n
Browse files Browse the repository at this point in the history
Duplicate data is accessed in the submission section,
pooled tasks list and claimed tasks list.
  • Loading branch information
kshepherd committed Jan 18, 2024
1 parent d8ed267 commit ab46576
Show file tree
Hide file tree
Showing 13 changed files with 281 additions and 17 deletions.
20 changes: 20 additions & 0 deletions src/app/core/data/item-data.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,26 @@ export abstract class BaseItemDataService extends IdentifiableDataService<Item>
);
}

public getDuplicatesEndpoint(itemId: string): Observable<string> {
return this.halService.getEndpoint(this.linkPath).pipe(
switchMap((url: string) => this.halService.getEndpoint('duplicates', `${url}/${itemId}`))
);
}

public getDuplicates(itemId: string, searchOptions?: PaginatedSearchOptions): Observable<RemoteData<PaginatedList<Item>>> {
const hrefObs = this.getDuplicatesEndpoint(itemId).pipe(
map((href) => searchOptions ? searchOptions.toRestUrl(href) : href)
);
hrefObs.pipe(
take(1)
).subscribe((href) => {
const request = new GetRequest(this.requestService.generateRequestId(), href);
this.requestService.send(request);
});

return this.rdbService.buildList<Item>(hrefObs);
}

/**
* Get the endpoint to move the item
* @param itemId
Expand Down
5 changes: 5 additions & 0 deletions src/app/core/shared/item.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -79,6 +80,7 @@ export class Item extends DSpaceObject implements ChildHALResource, HandleObject
thumbnail: HALLink;
accessStatus: HALLink;
identifiers: HALLink;
duplicates: HALLink;
self: HALLink;
};

Expand Down Expand Up @@ -131,6 +133,9 @@ export class Item extends DSpaceObject implements ChildHALResource, HandleObject
@link(IDENTIFIERS, false, 'identifiers')
identifiers?: Observable<RemoteData<IdentifierData>>;

@link(ITEM, true, 'duplicates')
duplicates?: Observable<RemoteData<PaginatedList<Duplicate>>>

/**
* Method that returns as which type of object this object should be rendered
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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[]
}
24 changes: 24 additions & 0 deletions src/app/shared/object-list/duplicate-data/duplicate.model.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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');
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -50,6 +51,11 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle
*/
public workflowitem$: BehaviorSubject<WorkflowItem> = new BehaviorSubject<WorkflowItem>(null);

/**
* The potential duplicates of this item
*/
public duplicates$: Observable<Duplicate[]>;

/**
* Display thumbnails if required by configuration
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@
[showSubmitter]="showSubmitter"
[badgeContext]="badgeContext"
[workflowItem]="workflowitem$.value"></ds-themed-item-list-preview>
<ng-container *ngVar="(duplicates$|async).length as duplicateCount">
<div [ngClass]="'col-md-12'" *ngIf="duplicateCount > 0">
<div class="d-flex">
{{ duplicateCount }} {{ 'submission.workflow.tasks.duplicates' | translate }}
</div>
</div>
</ng-container>

<div class="row">
<div [ngClass]="showThumbnails ? 'offset-3 offset-md-2 pl-3' : ''">
<ds-pool-task-actions id="actions"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';

import { BehaviorSubject, EMPTY, Observable } from 'rxjs';
import { mergeMap, tap } from 'rxjs/operators';
import {map, mergeMap, tap} from 'rxjs/operators';

import { ViewMode } from '../../../../core/shared/view-mode.model';
import { RemoteData } from '../../../../core/data/remote-data';
Expand All @@ -22,6 +22,8 @@ import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
import { Item } from '../../../../core/shared/item.model';
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";

/**
* This component renders pool task object for the search result in the list view.
Expand Down Expand Up @@ -55,6 +57,11 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen
*/
public workflowitem$: BehaviorSubject<WorkflowItem> = new BehaviorSubject<WorkflowItem>(null);

/**
* The potential duplicates of this workflow item
*/
public duplicates$: Observable<Duplicate[]>;

/**
* The index of this list element
*/
Expand All @@ -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'));

Expand All @@ -100,6 +107,19 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen
tap((itemRD: RemoteData<Item>) => {
if (isNotEmpty(itemRD) && itemRD.hasSucceeded) {
this.item$.next(itemRD.payload);
console.dir(itemRD.payload);
this.duplicates$ = itemRD.payload.duplicates.pipe(
getFirstCompletedRemoteData(),
map((remoteData: RemoteData<PaginatedList<Duplicate>>) => {
console.dir(remoteData);
if (remoteData.hasSucceeded) {
if (remoteData.payload.page) {
console.dir(remoteData.payload.page);
return remoteData.payload.page;
}
}
})
);
}
})
).subscribe();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!--
Template for the detect duplicates submission section component
@author Kim Shepherd
-->
<div class="text-sm-left" *ngVar="(this.data$ | async) as data">
<ng-container *ngIf="data.potentialDuplicates.length == 0">
<p>{{ 'submission.sections.duplicates.none' }}</p>
</ng-container>
<ng-container *ngIf="data.potentialDuplicates.length > 0">
<p>{{ 'submission.sections.duplicates.detected' | translate }}</p>
<div *ngFor="let dupe of data.potentialDuplicates" class="ds-duplicate">
<a target="_blank" [href]="'/items/'+dupe.uuid">{{dupe.title}}</a>
<div *ngFor="let metadatum of Metadata.toViewModelList(dupe.metadata)">
{{('item.preview.' + metadatum.key) | translate}} {{metadatum.value}}
</div>
<p *ngIf="dupe.workspaceItemId">{{ 'submission.sections.duplicates.in-workspace' | translate }}</p>
<p *ngIf="dupe.workflowItemId">{{ 'submission.sections.duplicates.in-workflow' | translate }}</p>
</div>
</ng-container>
</div>
127 changes: 127 additions & 0 deletions src/app/submission/sections/duplicates/section-duplicates.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import {ChangeDetectionStrategy, Component, Inject } from '@angular/core';

import { Observable, of as observableOf, Subscription } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { SectionsType } from '../sections-type';
import { SectionModelComponent } from '../models/section.model';
import { renderSectionFor } from '../sections-decorator';
import { SectionDataObject } from '../models/section-data.model';
import { SubmissionService } from '../../submission.service';
import { AlertType } from '../../../shared/alert/alert-type';
import { SectionsService } from '../sections.service';
import {map} from "rxjs/operators";
import {ItemDataService} from "../../../core/data/item-data.service";
import {
WorkspaceitemSectionDuplicatesObject
} from "../../../core/submission/models/workspaceitem-section-duplicates.model";
import {Metadata} from "../../../core/shared/metadata.utils";

/**
* Detect duplicates step
*
* @author Kim Shepherd
*/
@Component({
selector: 'ds-submission-section-duplicates',
templateUrl: './section-duplicates.component.html',
changeDetection: ChangeDetectionStrategy.Default
})

@renderSectionFor(SectionsType.Duplicates)
export class SubmissionSectionDuplicatesComponent extends SectionModelComponent {
/**
* The Alert categories.
* @type {AlertType}
*/
public AlertTypeEnum = AlertType;

/**
* Variable to track if the section is loading.
* @type {boolean}
*/
public isLoading = true;

/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
protected subs: Subscription[] = [];

/**
* Section data observable
*/
public data$: Observable<WorkspaceitemSectionDuplicatesObject>;

/**
* Initialize instance variables.
*
* @param {TranslateService} translate
* @param {SectionsService} sectionService
* @param {SubmissionService} submissionService
* @param itemDataService
* @param nameService
* @param {string} injectedCollectionId
* @param {SectionDataObject} injectedSectionData
* @param {string} injectedSubmissionId
*/
constructor(protected translate: TranslateService,
protected sectionService: SectionsService,
protected submissionService: SubmissionService,
private itemDataService: ItemDataService,
// private nameService: DSONameService,
@Inject('collectionIdProvider') public injectedCollectionId: string,
@Inject('sectionDataProvider') public injectedSectionData: SectionDataObject,
@Inject('submissionIdProvider') public injectedSubmissionId: string) {
super(injectedCollectionId, injectedSectionData, injectedSubmissionId);
}

ngOnInit() {
super.ngOnInit();
}

/**
* Initialize all instance variables and retrieve configuration.
*/
onSectionInit() {
this.isLoading = false;
this.data$ = this.getDuplicateData().pipe(
map((data: WorkspaceitemSectionDuplicatesObject) => {
console.dir(data);
return data;
})
);
}

/**
* Check if identifier section has read-only visibility
*/
isReadOnly(): boolean {
return true;
}

/**
* Unsubscribe from all subscriptions, if needed.
*/
onSectionDestroy(): void {
return;
}

/**
* Get section status. Because this simple component never requires human interaction, this is basically
* always going to be the opposite of "is this section still loading". This is not the place for API response
* error checking but determining whether the step can 'proceed'.
*
* @return Observable<boolean>
* the section status
*/
public getSectionStatus(): Observable<boolean> {
return observableOf(!this.isLoading);
}

public getDuplicateData(): Observable<WorkspaceitemSectionDuplicatesObject> {
return this.sectionService.getSectionData(this.submissionId, this.sectionData.id, this.sectionData.sectionType) as
Observable<WorkspaceitemSectionDuplicatesObject>;
}

protected readonly Metadata = Metadata;
}
1 change: 1 addition & 0 deletions src/app/submission/sections/sections-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export enum SectionsType {
SherpaPolicies = 'sherpaPolicy',
Identifiers = 'identifiers',
Collection = 'collection',
Duplicates = 'duplicates'
}
32 changes: 18 additions & 14 deletions src/app/submission/submission.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ import {
} from './sections/sherpa-policies/metadata-information/metadata-information.component';
import { SectionFormOperationsService } from './sections/form/section-form-operations.service';
import {SubmissionSectionIdentifiersComponent} from './sections/identifiers/section-identifiers.component';
import {SubmissionSectionDuplicatesComponent} from "./sections/duplicates/section-duplicates.component";
import {ItemSharedModule} from "../item-page/item-shared.module";

const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator
Expand All @@ -76,6 +78,7 @@ const ENTRY_COMPONENTS = [
SubmissionSectionCcLicensesComponent,
SubmissionSectionAccessesComponent,
SubmissionSectionSherpaPoliciesComponent,
SubmissionSectionDuplicatesComponent,
];

const DECLARATIONS = [
Expand Down Expand Up @@ -109,20 +112,21 @@ const DECLARATIONS = [
];

@NgModule({
imports: [
CommonModule,
CoreModule.forRoot(),
SharedModule,
StoreModule.forFeature('submission', submissionReducers, storeModuleConfig as StoreConfig<SubmissionState, Action>),
EffectsModule.forFeature(submissionEffects),
JournalEntitiesModule.withEntryComponents(),
ResearchEntitiesModule.withEntryComponents(),
FormModule,
NgbModalModule,
NgbCollapseModule,
NgbAccordionModule,
UploadModule,
],
imports: [
CommonModule,
CoreModule.forRoot(),
SharedModule,
StoreModule.forFeature('submission', submissionReducers, storeModuleConfig as StoreConfig<SubmissionState, Action>),
EffectsModule.forFeature(submissionEffects),
JournalEntitiesModule.withEntryComponents(),
ResearchEntitiesModule.withEntryComponents(),
FormModule,
NgbModalModule,
NgbCollapseModule,
NgbAccordionModule,
UploadModule,
ItemSharedModule,
],
declarations: DECLARATIONS,
exports: [
...DECLARATIONS,
Expand Down
Loading

0 comments on commit ab46576

Please sign in to comment.