Merge remote-tracking branch 'github/main' into coar-notify-7-part-two

This commit is contained in:
frabacche
2024-03-04 17:26:17 +01:00
29 changed files with 968 additions and 52 deletions

View File

@@ -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

View File

@@ -199,6 +199,7 @@ import { LdnService } from '../admin/admin-ldn-services/ldn-services-model/ldn-s
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 { AdminNotifyMessage } from '../admin/admin-notify-dashboard/models/admin-notify-message.model';
import { SubmissionDuplicateDataService } from './submission/submission-duplicate-data.service';
/**
* When not in production, endpoint responses can be mocked for testing purposes
@@ -235,6 +236,7 @@ const PROVIDERS = [
HALEndpointService,
HostWindowService,
ItemDataService,
SubmissionDuplicateDataService,
MetadataService,
ObjectCacheService,
PaginationComponentOptions,

View File

@@ -0,0 +1,8 @@
/*
* 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';
export interface WorkspaceitemSectionDuplicatesObject {
potentialDuplicates?: Duplicate[]
}

View File

@@ -3,8 +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';
/**
* An interface to represent submission's section object.
@@ -25,6 +26,7 @@ export type WorkspaceitemSectionDataType
| WorkspaceitemSectionAccessesObject
| WorkspaceitemSectionSherpaPoliciesObject
| WorkspaceitemSectionIdentifiersObject
| WorkspaceitemSectionDuplicatesObject
| string;

View File

@@ -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);
});
});
});

View File

@@ -0,0 +1,139 @@
/* eslint-disable max-classes-per-file */
import { Observable } from 'rxjs';
import { Injectable } from '@angular/core';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
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 '../data/search-response-parsing.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
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 '../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';
/**
* 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()
@dataService(DUPLICATE)
export class SubmissionDuplicateDataService extends BaseDataService<Duplicate> implements SearchData<Duplicate> {
/**
* The ResponseParsingService constructor name
*/
private parser: GenericConstructor<ResponseParsingService> = SearchResponseParsingService;
/**
* The RestRequest constructor name
*/
private request: GenericConstructor<RestRequest> = GetRequest;
/**
* SearchData interface to implement
* @private
*/
private searchData: SearchData<Duplicate>;
/**
* 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);
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<Duplicate>[]): Observable<RemoteData<PaginatedList<Duplicate>>> {
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
/**
* Helper method to get the duplicates endpoint
* @protected
*/
protected getEndpoint(): Observable<string> {
return this.halService.getEndpoint(this.linkPath);
}
/**
* Method to set service options
* @param {GenericConstructor<ResponseParsingService>} parser The ResponseParsingService constructor name
* @param {boolean} request The RestRequest constructor name
*/
setServiceOptions(parser: GenericConstructor<ResponseParsingService>, request: GenericConstructor<RestRequest>) {
if (parser) {
this.parser = parser;
}
if (request) {
this.request = request;
}
}
/**
* 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<Duplicate> 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<Duplicate>[]): Observable<RemoteData<PaginatedList<Duplicate>>> {
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 actual search/findByItem results
return this.searchBy('findByItem', findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
/**
* Unsubscribe from the subscription
*/
ngOnDestroy(): void {
if (this.sub !== undefined) {
this.sub.unsubscribe();
}
}
}

View File

@@ -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,

View File

@@ -0,0 +1,57 @@
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';
/**
* 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;
/**
* The item title
*/
@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 preview item (e.g. dc.title)
*/
@autoserialize
metadata: MetadataMap;
@autoserialize
type: ResourceType;
/**
* The {@link HALLink}s for the URL that generated this item (in context of search results)
*/
@deserialize
_links: {
self: HALLink;
};
}

View File

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

View File

@@ -4,7 +4,17 @@
[showSubmitter]="showSubmitter"
[badgeContext]="badgeContext"
[workflowItem]="workflowitem$.value"></ds-themed-item-list-preview>
<!-- Display duplicate alert, if feature enabled and duplicates detected -->
<ng-container *ngVar="(duplicates$|async)?.length as duplicateCount">
<div [ngClass]="'row'" *ngIf="duplicateCount > 0">
<div [ngClass]="'col-2'"></div>
<div [ngClass]="'col-10'">
<div class="d-flex alert alert-warning w-100">
{{ duplicateCount }} {{ 'submission.workflow.tasks.duplicates' | translate }}
</div>
</div>
</div>
</ng-container>
<div class="row">
<div [ngClass]="showThumbnails ? 'offset-3 offset-md-2 pl-3' : ''">
<ds-claimed-task-actions [item]="item$.value"

View File

@@ -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,10 @@ 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 { 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<ClaimedSearchResultListElementComponent>;
@@ -35,6 +39,21 @@ let fixture: ComponentFixture<ClaimedSearchResultListElementComponent>;
const mockResultObject: ClaimedTaskSearchResult = new ClaimedTaskSearchResult();
mockResultObject.hitHighlights = {};
const emptyList = createSuccessfulRemoteDataObject(createPaginatedList([]));
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
name: 'duplicate.enable',
values: [
'true'
]
}))
});
const duplicateDataServiceStub = {
findListByHref: () => observableOf(emptyList),
findDuplicates: () => createSuccessfulRemoteDataObject$({}),
};
const item = Object.assign(new Item(), {
bundles: observableOf({}),
metadata: {
@@ -83,7 +102,9 @@ describe('ClaimedSearchResultListElementComponent', () => {
{ provide: LinkService, useValue: linkService },
{ provide: DSONameService, useClass: DSONameServiceMock },
{ provide: APP_CONFIG, useValue: environment },
{ provide: ObjectCacheService, useValue: objectCacheServiceMock }
{ provide: ObjectCacheService, useValue: objectCacheServiceMock },
{ provide: ConfigurationDataService, useValue: configurationDataService },
{ provide: SubmissionDuplicateDataService, useValue: duplicateDataServiceStub },
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(ClaimedSearchResultListElementComponent, {

View File

@@ -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';
@@ -18,9 +18,14 @@ 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';
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',
@@ -50,6 +55,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
*/
@@ -60,6 +70,8 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle
protected truncatableService: TruncatableService,
public dsoNameService: DSONameService,
protected objectCache: ObjectCacheService,
protected configService: ConfigurationDataService,
protected duplicateDataService: SubmissionDuplicateDataService,
@Inject(APP_CONFIG) protected appConfig: AppConfig
) {
super(truncatableService, dsoNameService, appConfig);
@@ -93,8 +105,43 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle
}
})
).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<ConfigurationProperty>) => {
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<PaginatedList<Duplicate>>) => {
if (remoteData.hasSucceeded) {
if (remoteData.payload.page) {
return remoteData.payload.page;
}
}
})
);
} else {
return [] as Duplicate[];
}
}),
);
}
ngOnDestroy() {

View File

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

View File

@@ -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,10 @@ 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 { 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<PoolSearchResultListElementComponent>;
@@ -34,7 +38,23 @@ let fixture: ComponentFixture<PoolSearchResultListElementComponent>;
const mockResultObject: PoolTaskSearchResult = new PoolTaskSearchResult();
mockResultObject.hitHighlights = {};
const emptyList = createSuccessfulRemoteDataObject(createPaginatedList([]));
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
name: 'duplicate.enable',
values: [
'true'
]
}))
});
const duplicateDataServiceStub = {
findListByHref: () => observableOf(emptyList),
findDuplicates: () => createSuccessfulRemoteDataObject$({}),
};
const item = Object.assign(new Item(), {
duplicates: observableOf([]),
bundles: observableOf({}),
metadata: {
'dc.title': [
@@ -89,7 +109,9 @@ describe('PoolSearchResultListElementComponent', () => {
{ provide: LinkService, useValue: linkService },
{ provide: DSONameService, useClass: DSONameServiceMock },
{ provide: APP_CONFIG, useValue: environmentUseThumbs },
{ provide: ObjectCacheService, useValue: objectCacheServiceMock }
{ provide: ObjectCacheService, useValue: objectCacheServiceMock },
{ provide: ConfigurationDataService, useValue: configurationDataService },
{ provide: SubmissionDuplicateDataService, useValue: duplicateDataServiceStub }
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(PoolSearchResultListElementComponent, {

View File

@@ -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 {BehaviorSubject, combineLatest, EMPTY, Observable } from 'rxjs';
import { map, mergeMap, tap } from 'rxjs/operators';
import { ViewMode } from '../../../../core/shared/view-mode.model';
import { RemoteData } from '../../../../core/data/remote-data';
@@ -22,6 +22,11 @@ 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';
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.
@@ -55,6 +60,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
*/
@@ -70,6 +80,8 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen
protected truncatableService: TruncatableService,
public dsoNameService: DSONameService,
protected objectCache: ObjectCacheService,
protected configService: ConfigurationDataService,
protected duplicateDataService: SubmissionDuplicateDataService,
@Inject(APP_CONFIG) protected appConfig: AppConfig
) {
super(truncatableService, dsoNameService, appConfig);
@@ -101,10 +113,45 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen
if (isNotEmpty(itemRD) && itemRD.hasSucceeded) {
this.item$.next(itemRD.payload);
}
})
}),
).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<ConfigurationProperty>) => {
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<PaginatedList<Duplicate>>) => {
if (remoteData.hasSucceeded) {
if (remoteData.payload.page) {
return remoteData.payload.page;
}
}
})
);
} else {
return [] as Duplicate[];
}
}),
);
}
ngOnDestroy() {

View File

@@ -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;
}
@@ -845,6 +867,7 @@ export type SubmissionObjectAction = DisableSectionAction
| InitSubmissionFormAction
| ResetSubmissionFormAction
| CancelSubmissionFormAction
| CleanDuplicateDetectionAction
| CompleteInitSubmissionFormAction
| ChangeSubmissionCollectionAction
| SaveAndDepositSubmissionAction

View File

@@ -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,
@@ -43,18 +43,23 @@ import {
SubmissionObjectAction,
SubmissionObjectActionTypes,
UpdateSectionDataAction,
UpdateSectionDataSuccessAction
UpdateSectionDataSuccessAction,
CleanDuplicateDetectionAction
} 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';
import { environment } from '../../../environments/environment';
@Injectable()
export class SubmissionObjectEffects {
@@ -71,7 +76,12 @@ 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 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 && (alwaysDisplayDuplicates() || 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 +444,16 @@ 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 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) && sections.hasOwnProperty(duplicatesSectionId) && isEmpty((sections[duplicatesSectionId] as WorkspaceitemSectionDuplicatesObject).potentialDuplicates)) {
mappedActions.push(new CleanDuplicateDetectionAction(submissionId));
}
}
});
}
return mappedActions;
}
@@ -481,3 +499,7 @@ function filterErrors(sectionForm: FormState, sectionErrors: SubmissionSectionEr
});
return filteredErrors;
}
function alwaysDisplayDuplicates(): boolean {
return (environment.submission.duplicateDetection.alwaysShowSection);
}

View File

@@ -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();
});
});

View File

@@ -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 ]) && 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, {
[ 'duplicates' ]: Object.assign({}, state[ action.payload.submissionId ].sections.duplicates, {
enabled: false,
data: { potentialDuplicates: [] }
})
})
})
});
} else {
return state;
}
}

View File

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

View File

@@ -0,0 +1,248 @@
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, 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 { 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 { 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', {
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
})]
},
type: DUPLICATE,
_links: {
self: {
href: 'http://localhost:8080/server/api/core/submission/duplicates/search?uuid=testid'
}
}
}];
const sectionObject = {
header: 'submission.sections.submit.progressbar.duplicates',
mandatory: true,
opened: true,
data: {potentialDuplicates: duplicates},
errorsToShow: [],
serverValidationErrors: [],
id: 'duplicates',
sectionType: SectionsType.Duplicates,
sectionVisibility: null
};
describe('SubmissionSectionDuplicatesComponent test suite', () => {
let comp: SubmissionSectionDuplicatesComponent;
let compAsAny: any;
let fixture: ComponentFixture<SubmissionSectionDuplicatesComponent>;
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
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents().then();
}));
// First test to check the correct component creation
describe('', () => {
let testComp: TestComponent;
let testFixture: ComponentFixture<TestComponent>;
// synchronous beforeEach
beforeEach(() => {
sectionsServiceStub.isSectionReadOnly.and.returnValue(observableOf(false));
sectionsServiceStub.getSectionErrors.and.returnValue(observableOf([]));
sectionsServiceStub.getSectionData.and.returnValue(observableOf(sectionObject));
testFixture = TestBed.createComponent(SubmissionSectionDuplicatesComponent);
testComp = testFixture.componentInstance;
});
afterEach(() => {
testFixture.destroy();
});
it('should create SubmissionSectionDuplicatesComponent', () => {
expect(testComp).toBeTruthy();
});
});
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: duplicates}));
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 {
}

View File

@@ -0,0 +1,124 @@
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 {
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
*
* @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 {
protected readonly Metadata = Metadata;
/**
* 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[] = [];
/**
* Initialize instance variables.
*
* @param {TranslateService} translate
* @param {SectionsService} sectionService
* @param {SubmissionService} submissionService
* @param {string} injectedCollectionId
* @param {SectionDataObject} injectedSectionData
* @param {string} injectedSubmissionId
*/
constructor(protected translate: TranslateService,
protected sectionService: SectionsService,
protected submissionService: SubmissionService,
@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;
}
/**
* 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);
}
/**
* Get duplicate data as observable from the section data
*/
public getDuplicateData(): Observable<WorkspaceitemSectionDuplicatesObject> {
return this.sectionService.getSectionData(this.submissionId, this.sectionData.id, this.sectionData.sectionType) as
Observable<WorkspaceitemSectionDuplicatesObject>;
}
/**
* 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();
}
}

View File

@@ -9,5 +9,6 @@ export enum SectionsType {
SherpaPolicies = 'sherpaPolicy',
Identifiers = 'identifiers',
Collection = 'collection',
CoarNotify = 'coarnotify'
CoarNotify = 'coarnotify',
Duplicates = 'duplicates'
}

View File

@@ -66,7 +66,8 @@ import {
MetadataInformationComponent
} 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 { SubmissionSectionIdentifiersComponent } from './sections/identifiers/section-identifiers.component';
import { SubmissionSectionDuplicatesComponent } from './sections/duplicates/section-duplicates.component';
import { SubmissionSectionCoarNotifyComponent } from './sections/section-coar-notify/section-coar-notify.component';
import {
CoarNotifyConfigDataService
@@ -81,7 +82,8 @@ const ENTRY_COMPONENTS = [
SubmissionSectionCcLicensesComponent,
SubmissionSectionAccessesComponent,
SubmissionSectionSherpaPoliciesComponent,
SubmissionSectionCoarNotifyComponent
SubmissionSectionCoarNotifyComponent,
SubmissionSectionDuplicatesComponent
];
const DECLARATIONS = [
@@ -102,6 +104,7 @@ const DECLARATIONS = [
SubmissionSectionUploadFileEditComponent,
SubmissionSectionUploadFileViewComponent,
SubmissionSectionIdentifiersComponent,
SubmissionSectionDuplicatesComponent,
SubmissionImportExternalComponent,
ThemedSubmissionImportExternalComponent,
SubmissionImportExternalSearchbarComponent,

View File

@@ -2826,6 +2826,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",
@@ -5106,7 +5108,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",
@@ -5238,6 +5240,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",
@@ -5364,6 +5374,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.",

View File

@@ -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');

View File

@@ -157,6 +157,9 @@ export class DefaultAppConfig implements AppConfig {
*/
timer: 0
},
duplicateDetection: {
alwaysShowSection: false
},
typeBind: {
field: 'dc.type'
},

View File

@@ -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;
}

View File

@@ -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'
},