Skip to content

Commit e76b6c9

Browse files
committed
[TLC-674] Duplicate detection frontend changes as per feedback
1 parent 6229966 commit e76b6c9

10 files changed

Lines changed: 154 additions & 56 deletions

src/app/core/core.module.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ import { NotifyRequestsStatus } from '../item-page/simple/notify-requests-status
198198
import { LdnService } from '../admin/admin-ldn-services/ldn-services-model/ldn-services.model';
199199
import { Itemfilter } from '../admin/admin-ldn-services/ldn-services-model/ldn-service-itemfilters';
200200
import { SubmissionCoarNotifyConfig } from '../submission/sections/section-coar-notify/submission-coar-notify.config';
201-
import { DuplicateDataService } from './data/duplicate-search.service';
201+
import { SubmissionDuplicateDataService } from './submission/submission-duplicate-data.service';
202202

203203
/**
204204
* When not in production, endpoint responses can be mocked for testing purposes
@@ -235,7 +235,7 @@ const PROVIDERS = [
235235
HALEndpointService,
236236
HostWindowService,
237237
ItemDataService,
238-
DuplicateDataService,
238+
SubmissionDuplicateDataService,
239239
MetadataService,
240240
ObjectCacheService,
241241
PaginationComponentOptions,

src/app/core/data/item-data.service.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,6 @@ import { RestRequestMethod } from './rest-request-method';
4646
import { CreateData, CreateDataImpl } from './base/create-data';
4747
import { RequestParam } from '../cache/models/request-param.model';
4848
import { dataService } from './base/data-service.decorator';
49-
import { Duplicate } from '../../shared/object-list/duplicate-data/duplicate.model';
50-
import { SearchDataImpl } from './base/search-data';
51-
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
5249

5350
/**
5451
* An abstract service for CRUD operations on Items
@@ -59,7 +56,6 @@ export abstract class BaseItemDataService extends IdentifiableDataService<Item>
5956
private createData: CreateData<Item>;
6057
private patchData: PatchData<Item>;
6158
private deleteData: DeleteData<Item>;
62-
private searchData: SearchDataImpl<Duplicate>;
6359

6460
protected constructor(
6561
protected linkPath,
@@ -78,7 +74,6 @@ export abstract class BaseItemDataService extends IdentifiableDataService<Item>
7874
this.createData = new CreateDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive);
7975
this.patchData = new PatchDataImpl<Item>(this.linkPath, requestService, rdbService, objectCache, halService, comparator, this.responseMsToLive, this.constructIdEndpoint);
8076
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
81-
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
8277
}
8378

8479
/**
@@ -247,20 +242,6 @@ export abstract class BaseItemDataService extends IdentifiableDataService<Item>
247242
);
248243
}
249244

250-
public findDuplicates(uuid: string, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Duplicate>[]): Observable<RemoteData<PaginatedList<Duplicate>>> {
251-
const searchParams = [new RequestParam('uuid', uuid)];
252-
let findListOptions = new FindListOptions();
253-
if (options) {
254-
findListOptions = Object.assign(new FindListOptions(), options);
255-
}
256-
if (findListOptions.searchParams) {
257-
findListOptions.searchParams = [...findListOptions.searchParams, ...searchParams];
258-
} else {
259-
findListOptions.searchParams = searchParams;
260-
}
261-
return this.searchData.searchBy('findDuplicates', findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
262-
}
263-
264245
/**
265246
* Get the endpoint to move the item
266247
* @param itemId

src/app/core/data/duplicate-search.service.ts renamed to src/app/core/submission/submission-duplicate-data.service.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,34 @@ import { Observable } from 'rxjs';
33
import { Injectable } from '@angular/core';
44
import { map } from 'rxjs/operators';
55
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
6-
import { ResponseParsingService } from './parsing.service';
7-
import { RemoteData } from './remote-data';
8-
import { GetRequest } from './request.models';
9-
import { RequestService } from './request.service';
6+
import { ResponseParsingService } from '../data/parsing.service';
7+
import { RemoteData } from '../data/remote-data';
8+
import { GetRequest } from '../data/request.models';
9+
import { RequestService } from '../data/request.service';
1010
import { GenericConstructor } from '../shared/generic-constructor';
1111
import { HALEndpointService } from '../shared/hal-endpoint.service';
12-
import { SearchResponseParsingService } from './search-response-parsing.service';
12+
import { SearchResponseParsingService } from '../data/search-response-parsing.service';
1313
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
14-
import { RestRequest } from './rest-request.model';
15-
import { BaseDataService } from './base/base-data.service';
16-
import { FindListOptions } from './find-list-options.model';
14+
import { RestRequest } from '../data/rest-request.model';
15+
import { BaseDataService } from '../data/base/base-data.service';
16+
import { FindListOptions } from '../data/find-list-options.model';
1717
import { Duplicate } from '../../shared/object-list/duplicate-data/duplicate.model';
18-
import { PaginatedList } from './paginated-list.model';
18+
import { PaginatedList } from '../data/paginated-list.model';
1919
import { RequestParam } from '../cache/models/request-param.model';
2020
import { ObjectCacheService } from '../cache/object-cache.service';
2121

2222

2323
/**
24-
* Service that performs all general actions that have to do with the search page
24+
* Service that handles search requests for potential duplicate items.
25+
* This uses the /api/submission/duplicates endpoint to look for other archived or in-progress items (if user
26+
* has READ permission) that match the item (for the given uuid).
27+
* Matching is configured in the backend in dspace/config/modulesduplicate-detection.cfg
28+
* The returned results are small preview 'stubs' of items, and displayed in either a submission section
29+
* or the workflow pooled/claimed task page.
30+
*
2531
*/
2632
@Injectable()
27-
export class DuplicateDataService extends BaseDataService<Duplicate> {
33+
export class SubmissionDuplicateDataService extends BaseDataService<Duplicate> {
2834

2935
/**
3036
* The ResponseParsingService constructor name

src/app/shared/object-list/duplicate-data/duplicate.model.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import { CacheableObject } from '../../../core/cache/cacheable-object.model';
55
import { DUPLICATE } from './duplicate.resource-type';
66
import { ResourceType } from '../../../core/shared/resource-type';
77

8+
/**
9+
* This implements the model of a duplicate preview stub, to be displayed to submitters or reviewers
10+
* if duplicate detection is enabled. The metadata map is configurable in the backend at duplicate-detection.cfg
11+
*/
812
export class Duplicate implements CacheableObject {
913

1014
static type = DUPLICATE;
@@ -14,17 +18,28 @@ export class Duplicate implements CacheableObject {
1418
*/
1519
@autoserialize
1620
title: string;
21+
/**
22+
* The item uuid
23+
*/
1724
@autoserialize
1825
uuid: string;
26+
/**
27+
* The workfow item ID, if any
28+
*/
1929
@autoserialize
2030
workflowItemId: number;
31+
/**
32+
* The workspace item ID, if any
33+
*/
2134
@autoserialize
2235
workspaceItemId: number;
36+
/**
37+
* The owning collection of the item
38+
*/
2339
@autoserialize
2440
owningCollection: string;
25-
2641
/**
27-
* Metadata for the bitstream (e.g. dc.description)
42+
* Metadata for the preview item (e.g. dc.title)
2843
*/
2944
@autoserialize
3045
metadata: MetadataMap;
@@ -33,7 +48,7 @@ export class Duplicate implements CacheableObject {
3348
type: ResourceType;
3449

3550
/**
36-
* The {@link HALLink}s for this Bitstream
51+
* The {@link HALLink}s for the URL that generated this item (in context of search results)
3752
*/
3853
@deserialize
3954
_links: {

src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ import { ObjectCacheService } from '../../../../core/cache/object-cache.service'
3030
import { Context } from '../../../../core/shared/context.model';
3131
import { createPaginatedList } from '../../../testing/utils.test';
3232
import { ItemDataService } from '../../../../core/data/item-data.service';
33-
import { DuplicateDataService } from '../../../../core/data/duplicate-search.service';
33+
import { SubmissionDuplicateDataService } from '../../../../core/submission/submission-duplicate-data.service';
34+
import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model';
35+
import { ConfigurationDataService } from '../../../../core/data/configuration-data.service';
3436

3537
let component: ClaimedSearchResultListElementComponent;
3638
let fixture: ComponentFixture<ClaimedSearchResultListElementComponent>;
@@ -41,8 +43,15 @@ mockResultObject.hitHighlights = {};
4143
const emptyList = createSuccessfulRemoteDataObject(createPaginatedList([]));
4244
const itemDataServiceStub = {
4345
findListByHref: () => observableOf(emptyList),
44-
4546
};
47+
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
48+
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
49+
name: 'duplicate.enable',
50+
values: [
51+
'true'
52+
]
53+
}))
54+
});
4655
const duplicateDataServiceStub = {
4756
findListByHref: () => observableOf(emptyList),
4857
findDuplicates: () => createSuccessfulRemoteDataObject$({}),
@@ -98,7 +107,8 @@ describe('ClaimedSearchResultListElementComponent', () => {
98107
{ provide: APP_CONFIG, useValue: environment },
99108
{ provide: ObjectCacheService, useValue: objectCacheServiceMock },
100109
{ provide: ItemDataService, useValue: itemDataServiceStub },
101-
{ provide: DuplicateDataService, useValue: duplicateDataServiceStub },
110+
{ provide: ConfigurationDataService, useValue: configurationDataService },
111+
{ provide: SubmissionDuplicateDataService, useValue: duplicateDataServiceStub },
102112
],
103113
schemas: [NO_ERRORS_SCHEMA]
104114
}).overrideComponent(ClaimedSearchResultListElementComponent, {

src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { listableObjectComponent } from '../../../object-collection/shared/lista
55
import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model';
66
import { LinkService } from '../../../../core/cache/builders/link.service';
77
import { TruncatableService } from '../../../truncatable/truncatable.service';
8-
import { BehaviorSubject, EMPTY, Observable } from 'rxjs';
8+
import {BehaviorSubject, combineLatest, EMPTY, Observable} from 'rxjs';
99
import { RemoteData } from '../../../../core/data/remote-data';
1010
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
1111
import { followLink } from '../../../utils/follow-link-config.model';
@@ -24,7 +24,9 @@ import { Context } from '../../../../core/shared/context.model';
2424
import { Duplicate } from '../../duplicate-data/duplicate.model';
2525
import { PaginatedList } from '../../../../core/data/paginated-list.model';
2626
import { ItemDataService } from '../../../../core/data/item-data.service';
27-
import { DuplicateDataService } from '../../../../core/data/duplicate-search.service';
27+
import { SubmissionDuplicateDataService } from '../../../../core/submission/submission-duplicate-data.service';
28+
import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model';
29+
import { ConfigurationDataService } from '../../../../core/data/configuration-data.service';
2830

2931
@Component({
3032
selector: 'ds-claimed-search-result-list-element',
@@ -57,7 +59,7 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle
5759
/**
5860
* The potential duplicates of this item
5961
*/
60-
public duplicates$: Observable<Duplicate[]> = new Observable<Duplicate[]>();
62+
public duplicates$: Observable<Duplicate[]>;
6163

6264
/**
6365
* Display thumbnails if required by configuration
@@ -70,7 +72,8 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle
7072
public dsoNameService: DSONameService,
7173
protected objectCache: ObjectCacheService,
7274
protected itemDataService: ItemDataService,
73-
protected duplicateDataService: DuplicateDataService,
75+
protected configService: ConfigurationDataService,
76+
protected duplicateDataService: SubmissionDuplicateDataService,
7477
@Inject(APP_CONFIG) protected appConfig: AppConfig
7578
) {
7679
super(truncatableService, dsoNameService, appConfig);
@@ -114,10 +117,45 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle
114117
}
115118
})
116119
).subscribe();
117-
120+
// Initialise duplicates, if enabled
121+
this.duplicates$ = this.initializeDuplicateDetectionIfEnabled();
118122
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
119123
}
120124

125+
/**
126+
* Initialize and set the duplicates observable based on whether the configuration in REST is enabled
127+
* and the results returned
128+
*/
129+
initializeDuplicateDetectionIfEnabled() {
130+
return combineLatest([
131+
this.configService.findByPropertyName('duplicate.enable').pipe(
132+
getFirstCompletedRemoteData(),
133+
map((remoteData: RemoteData<ConfigurationProperty>) => {
134+
return (remoteData.isSuccess && remoteData.payload && remoteData.payload.values[0] === 'true');
135+
})
136+
),
137+
this.item$.pipe(),
138+
]
139+
).pipe(
140+
map(([enabled, rd]) => {
141+
if (enabled) {
142+
this.duplicates$ = this.duplicateDataService.findDuplicates(rd.uuid).pipe(
143+
getFirstCompletedRemoteData(),
144+
map((remoteData: RemoteData<PaginatedList<Duplicate>>) => {
145+
if (remoteData.hasSucceeded) {
146+
if (remoteData.payload.page) {
147+
return remoteData.payload.page;
148+
}
149+
}
150+
})
151+
);
152+
} else {
153+
return [] as Duplicate[];
154+
}
155+
}),
156+
);
157+
}
158+
121159
ngOnDestroy() {
122160
// This ensures the object is removed from cache, when action is performed on task
123161
if (hasValue(this.dso)) {

src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
[showSubmitter]="showSubmitter"
55
[badgeContext]="badgeContext"
66
[workflowItem]="workflowitem$.value"></ds-themed-item-list-preview>
7+
78
<!-- Display duplicate alert, if feature enabled and duplicates detected -->
89
<ng-container *ngVar="(duplicates$|async)?.length as duplicateCount">
910
<div [ngClass]="'row'" *ngIf="duplicateCount > 0">

src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ import { ObjectCacheService } from '../../../../core/cache/object-cache.service'
2929
import { Context } from '../../../../core/shared/context.model';
3030
import { createPaginatedList } from '../../../testing/utils.test';
3131
import { ItemDataService } from '../../../../core/data/item-data.service';
32-
import { DuplicateDataService } from '../../../../core/data/duplicate-search.service';
32+
import { SubmissionDuplicateDataService } from '../../../../core/submission/submission-duplicate-data.service';
33+
import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model';
34+
import { ConfigurationDataService } from '../../../../core/data/configuration-data.service';
3335

3436
let component: PoolSearchResultListElementComponent;
3537
let fixture: ComponentFixture<PoolSearchResultListElementComponent>;
@@ -41,6 +43,14 @@ const emptyList = createSuccessfulRemoteDataObject(createPaginatedList([]));
4143
const itemDataServiceStub = {
4244
findListByHref: () => observableOf(emptyList),
4345
};
46+
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
47+
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
48+
name: 'duplicate.enable',
49+
values: [
50+
'true'
51+
]
52+
}))
53+
});
4454
const duplicateDataServiceStub = {
4555
findListByHref: () => observableOf(emptyList),
4656
findDuplicates: () => createSuccessfulRemoteDataObject$({}),
@@ -104,7 +114,8 @@ describe('PoolSearchResultListElementComponent', () => {
104114
{ provide: APP_CONFIG, useValue: environmentUseThumbs },
105115
{ provide: ObjectCacheService, useValue: objectCacheServiceMock },
106116
{ provide: ItemDataService, useValue: itemDataServiceStub },
107-
{ provide: DuplicateDataService, useValue: duplicateDataServiceStub }
117+
{ provide: ConfigurationDataService, useValue: configurationDataService },
118+
{ provide: SubmissionDuplicateDataService, useValue: duplicateDataServiceStub }
108119
],
109120
schemas: [NO_ERRORS_SCHEMA]
110121
}).overrideComponent(PoolSearchResultListElementComponent, {

0 commit comments

Comments
 (0)