mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge remote-tracking branch 'github/main' into coar-notify-7-part-two
This commit is contained in:
@@ -131,6 +131,10 @@ submission:
|
|||||||
# NOTE: after how many time (milliseconds) submission is saved automatically
|
# NOTE: after how many time (milliseconds) submission is saved automatically
|
||||||
# eg. timer: 5 * (1000 * 60); // 5 minutes
|
# eg. timer: 5 * (1000 * 60); // 5 minutes
|
||||||
timer: 0
|
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:
|
icons:
|
||||||
metadata:
|
metadata:
|
||||||
# NOTE: example of configuration
|
# NOTE: example of configuration
|
||||||
|
@@ -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 { 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 { SubmissionCoarNotifyConfig } from '../submission/sections/section-coar-notify/submission-coar-notify.config';
|
||||||
import { AdminNotifyMessage } from '../admin/admin-notify-dashboard/models/admin-notify-message.model';
|
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
|
* When not in production, endpoint responses can be mocked for testing purposes
|
||||||
@@ -235,6 +236,7 @@ const PROVIDERS = [
|
|||||||
HALEndpointService,
|
HALEndpointService,
|
||||||
HostWindowService,
|
HostWindowService,
|
||||||
ItemDataService,
|
ItemDataService,
|
||||||
|
SubmissionDuplicateDataService,
|
||||||
MetadataService,
|
MetadataService,
|
||||||
ObjectCacheService,
|
ObjectCacheService,
|
||||||
PaginationComponentOptions,
|
PaginationComponentOptions,
|
||||||
|
@@ -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[]
|
||||||
|
}
|
@@ -3,8 +3,9 @@ import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.mod
|
|||||||
import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model';
|
import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model';
|
||||||
import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model';
|
import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model';
|
||||||
import { WorkspaceitemSectionCcLicenseObject } from './workspaceitem-section-cc-license.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 { WorkspaceitemSectionSherpaPoliciesObject } from './workspaceitem-section-sherpa-policies.model';
|
||||||
|
import { WorkspaceitemSectionDuplicatesObject } from './workspaceitem-section-duplicates.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An interface to represent submission's section object.
|
* An interface to represent submission's section object.
|
||||||
@@ -25,6 +26,7 @@ export type WorkspaceitemSectionDataType
|
|||||||
| WorkspaceitemSectionAccessesObject
|
| WorkspaceitemSectionAccessesObject
|
||||||
| WorkspaceitemSectionSherpaPoliciesObject
|
| WorkspaceitemSectionSherpaPoliciesObject
|
||||||
| WorkspaceitemSectionIdentifiersObject
|
| WorkspaceitemSectionIdentifiersObject
|
||||||
|
| WorkspaceitemSectionDuplicatesObject
|
||||||
| string;
|
| string;
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
139
src/app/core/submission/submission-duplicate-data.service.ts
Normal file
139
src/app/core/submission/submission-duplicate-data.service.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1114,7 +1114,10 @@ export const mockSubmissionState: SubmissionObjectState = Object.assign({}, {
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
isValid: false,
|
isValid: false,
|
||||||
removePending: false
|
removePending: false
|
||||||
} as any
|
} as any,
|
||||||
|
'duplicates': {
|
||||||
|
potentialDuplicates: []
|
||||||
|
} as any,
|
||||||
},
|
},
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
savePending: false,
|
savePending: false,
|
||||||
|
57
src/app/shared/object-list/duplicate-data/duplicate.model.ts
Normal file
57
src/app/shared/object-list/duplicate-data/duplicate.model.ts
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
@@ -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');
|
@@ -4,7 +4,17 @@
|
|||||||
[showSubmitter]="showSubmitter"
|
[showSubmitter]="showSubmitter"
|
||||||
[badgeContext]="badgeContext"
|
[badgeContext]="badgeContext"
|
||||||
[workflowItem]="workflowitem$.value"></ds-themed-item-list-preview>
|
[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 class="row">
|
||||||
<div [ngClass]="showThumbnails ? 'offset-3 offset-md-2 pl-3' : ''">
|
<div [ngClass]="showThumbnails ? 'offset-3 offset-md-2 pl-3' : ''">
|
||||||
<ds-claimed-task-actions [item]="item$.value"
|
<ds-claimed-task-actions [item]="item$.value"
|
||||||
|
@@ -15,7 +15,7 @@ import { Item } from '../../../../core/shared/item.model';
|
|||||||
import { ClaimedSearchResultListElementComponent } from './claimed-search-result-list-element.component';
|
import { ClaimedSearchResultListElementComponent } from './claimed-search-result-list-element.component';
|
||||||
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
|
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
|
||||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.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 { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model';
|
||||||
import { TruncatableService } from '../../../truncatable/truncatable.service';
|
import { TruncatableService } from '../../../truncatable/truncatable.service';
|
||||||
import { VarDirective } from '../../../utils/var.directive';
|
import { VarDirective } from '../../../utils/var.directive';
|
||||||
@@ -28,6 +28,10 @@ import { APP_CONFIG } from '../../../../../config/app-config.interface';
|
|||||||
import { environment } from '../../../../../environments/environment';
|
import { environment } from '../../../../../environments/environment';
|
||||||
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
||||||
import { Context } from '../../../../core/shared/context.model';
|
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 component: ClaimedSearchResultListElementComponent;
|
||||||
let fixture: ComponentFixture<ClaimedSearchResultListElementComponent>;
|
let fixture: ComponentFixture<ClaimedSearchResultListElementComponent>;
|
||||||
@@ -35,6 +39,21 @@ let fixture: ComponentFixture<ClaimedSearchResultListElementComponent>;
|
|||||||
const mockResultObject: ClaimedTaskSearchResult = new ClaimedTaskSearchResult();
|
const mockResultObject: ClaimedTaskSearchResult = new ClaimedTaskSearchResult();
|
||||||
mockResultObject.hitHighlights = {};
|
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(), {
|
const item = Object.assign(new Item(), {
|
||||||
bundles: observableOf({}),
|
bundles: observableOf({}),
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -83,7 +102,9 @@ describe('ClaimedSearchResultListElementComponent', () => {
|
|||||||
{ provide: LinkService, useValue: linkService },
|
{ provide: LinkService, useValue: linkService },
|
||||||
{ provide: DSONameService, useClass: DSONameServiceMock },
|
{ provide: DSONameService, useClass: DSONameServiceMock },
|
||||||
{ provide: APP_CONFIG, useValue: environment },
|
{ 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]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(ClaimedSearchResultListElementComponent, {
|
}).overrideComponent(ClaimedSearchResultListElementComponent, {
|
||||||
|
@@ -5,7 +5,7 @@ import { listableObjectComponent } from '../../../object-collection/shared/lista
|
|||||||
import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model';
|
import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model';
|
||||||
import { LinkService } from '../../../../core/cache/builders/link.service';
|
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||||
import { TruncatableService } from '../../../truncatable/truncatable.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 { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||||
import { followLink } from '../../../utils/follow-link-config.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 { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
||||||
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
|
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
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 { isNotEmpty, hasValue } from '../../../empty.util';
|
||||||
import { Context } from '../../../../core/shared/context.model';
|
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({
|
@Component({
|
||||||
selector: 'ds-claimed-search-result-list-element',
|
selector: 'ds-claimed-search-result-list-element',
|
||||||
@@ -50,6 +55,11 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle
|
|||||||
*/
|
*/
|
||||||
public workflowitem$: BehaviorSubject<WorkflowItem> = new BehaviorSubject<WorkflowItem>(null);
|
public workflowitem$: BehaviorSubject<WorkflowItem> = new BehaviorSubject<WorkflowItem>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The potential duplicates of this item
|
||||||
|
*/
|
||||||
|
public duplicates$: Observable<Duplicate[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display thumbnails if required by configuration
|
* Display thumbnails if required by configuration
|
||||||
*/
|
*/
|
||||||
@@ -60,6 +70,8 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle
|
|||||||
protected truncatableService: TruncatableService,
|
protected truncatableService: TruncatableService,
|
||||||
public dsoNameService: DSONameService,
|
public dsoNameService: DSONameService,
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected configService: ConfigurationDataService,
|
||||||
|
protected duplicateDataService: SubmissionDuplicateDataService,
|
||||||
@Inject(APP_CONFIG) protected appConfig: AppConfig
|
@Inject(APP_CONFIG) protected appConfig: AppConfig
|
||||||
) {
|
) {
|
||||||
super(truncatableService, dsoNameService, appConfig);
|
super(truncatableService, dsoNameService, appConfig);
|
||||||
@@ -93,8 +105,43 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
).subscribe();
|
).subscribe();
|
||||||
|
|
||||||
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
|
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() {
|
ngOnDestroy() {
|
||||||
|
@@ -4,6 +4,19 @@
|
|||||||
[showSubmitter]="showSubmitter"
|
[showSubmitter]="showSubmitter"
|
||||||
[badgeContext]="badgeContext"
|
[badgeContext]="badgeContext"
|
||||||
[workflowItem]="workflowitem$.value"></ds-themed-item-list-preview>
|
[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 class="row">
|
||||||
<div [ngClass]="showThumbnails ? 'offset-3 offset-md-2 pl-3' : ''">
|
<div [ngClass]="showThumbnails ? 'offset-3 offset-md-2 pl-3' : ''">
|
||||||
<ds-pool-task-actions id="actions"
|
<ds-pool-task-actions id="actions"
|
||||||
|
@@ -15,7 +15,7 @@ import { Item } from '../../../../core/shared/item.model';
|
|||||||
import { PoolSearchResultListElementComponent } from './pool-search-result-list-element.component';
|
import { PoolSearchResultListElementComponent } from './pool-search-result-list-element.component';
|
||||||
import { PoolTask } from '../../../../core/tasks/models/pool-task-object.model';
|
import { PoolTask } from '../../../../core/tasks/models/pool-task-object.model';
|
||||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.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 { PoolTaskSearchResult } from '../../../object-collection/shared/pool-task-search-result.model';
|
||||||
import { TruncatableService } from '../../../truncatable/truncatable.service';
|
import { TruncatableService } from '../../../truncatable/truncatable.service';
|
||||||
import { VarDirective } from '../../../utils/var.directive';
|
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 { APP_CONFIG } from '../../../../../config/app-config.interface';
|
||||||
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
||||||
import { Context } from '../../../../core/shared/context.model';
|
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 component: PoolSearchResultListElementComponent;
|
||||||
let fixture: ComponentFixture<PoolSearchResultListElementComponent>;
|
let fixture: ComponentFixture<PoolSearchResultListElementComponent>;
|
||||||
@@ -34,7 +38,23 @@ let fixture: ComponentFixture<PoolSearchResultListElementComponent>;
|
|||||||
const mockResultObject: PoolTaskSearchResult = new PoolTaskSearchResult();
|
const mockResultObject: PoolTaskSearchResult = new PoolTaskSearchResult();
|
||||||
mockResultObject.hitHighlights = {};
|
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(), {
|
const item = Object.assign(new Item(), {
|
||||||
|
duplicates: observableOf([]),
|
||||||
bundles: observableOf({}),
|
bundles: observableOf({}),
|
||||||
metadata: {
|
metadata: {
|
||||||
'dc.title': [
|
'dc.title': [
|
||||||
@@ -89,7 +109,9 @@ describe('PoolSearchResultListElementComponent', () => {
|
|||||||
{ provide: LinkService, useValue: linkService },
|
{ provide: LinkService, useValue: linkService },
|
||||||
{ provide: DSONameService, useClass: DSONameServiceMock },
|
{ provide: DSONameService, useClass: DSONameServiceMock },
|
||||||
{ provide: APP_CONFIG, useValue: environmentUseThumbs },
|
{ 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]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(PoolSearchResultListElementComponent, {
|
}).overrideComponent(PoolSearchResultListElementComponent, {
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
|
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
|
||||||
import { BehaviorSubject, EMPTY, Observable } from 'rxjs';
|
import {BehaviorSubject, combineLatest, 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 { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
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 { Item } from '../../../../core/shared/item.model';
|
||||||
import { isNotEmpty, hasValue } from '../../../empty.util';
|
import { isNotEmpty, hasValue } from '../../../empty.util';
|
||||||
import { Context } from '../../../../core/shared/context.model';
|
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.
|
* 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);
|
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
|
* The index of this list element
|
||||||
*/
|
*/
|
||||||
@@ -70,6 +80,8 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen
|
|||||||
protected truncatableService: TruncatableService,
|
protected truncatableService: TruncatableService,
|
||||||
public dsoNameService: DSONameService,
|
public dsoNameService: DSONameService,
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected configService: ConfigurationDataService,
|
||||||
|
protected duplicateDataService: SubmissionDuplicateDataService,
|
||||||
@Inject(APP_CONFIG) protected appConfig: AppConfig
|
@Inject(APP_CONFIG) protected appConfig: AppConfig
|
||||||
) {
|
) {
|
||||||
super(truncatableService, dsoNameService, appConfig);
|
super(truncatableService, dsoNameService, appConfig);
|
||||||
@@ -101,10 +113,45 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen
|
|||||||
if (isNotEmpty(itemRD) && itemRD.hasSucceeded) {
|
if (isNotEmpty(itemRD) && itemRD.hasSucceeded) {
|
||||||
this.item$.next(itemRD.payload);
|
this.item$.next(itemRD.payload);
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
).subscribe();
|
).subscribe();
|
||||||
|
|
||||||
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
|
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() {
|
ngOnDestroy() {
|
||||||
|
@@ -56,6 +56,9 @@ export const SubmissionObjectActionTypes = {
|
|||||||
DISCARD_SUBMISSION_SUCCESS: type('dspace/submission/DISCARD_SUBMISSION_SUCCESS'),
|
DISCARD_SUBMISSION_SUCCESS: type('dspace/submission/DISCARD_SUBMISSION_SUCCESS'),
|
||||||
DISCARD_SUBMISSION_ERROR: type('dspace/submission/DISCARD_SUBMISSION_ERROR'),
|
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
|
// Upload file types
|
||||||
NEW_FILE: type('dspace/submission/NEW_FILE'),
|
NEW_FILE: type('dspace/submission/NEW_FILE'),
|
||||||
EDIT_FILE_DATA: type('dspace/submission/EDIT_FILE_DATA'),
|
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 {
|
export class UpdateSectionDataSuccessAction implements Action {
|
||||||
type = SubmissionObjectActionTypes.UPDATE_SECTION_DATA_SUCCESS;
|
type = SubmissionObjectActionTypes.UPDATE_SECTION_DATA_SUCCESS;
|
||||||
}
|
}
|
||||||
@@ -845,6 +867,7 @@ export type SubmissionObjectAction = DisableSectionAction
|
|||||||
| InitSubmissionFormAction
|
| InitSubmissionFormAction
|
||||||
| ResetSubmissionFormAction
|
| ResetSubmissionFormAction
|
||||||
| CancelSubmissionFormAction
|
| CancelSubmissionFormAction
|
||||||
|
| CleanDuplicateDetectionAction
|
||||||
| CompleteInitSubmissionFormAction
|
| CompleteInitSubmissionFormAction
|
||||||
| ChangeSubmissionCollectionAction
|
| ChangeSubmissionCollectionAction
|
||||||
| SaveAndDepositSubmissionAction
|
| SaveAndDepositSubmissionAction
|
||||||
|
@@ -1,25 +1,25 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
import {Actions, createEffect, ofType} from '@ngrx/effects';
|
||||||
import { Store } from '@ngrx/store';
|
import {Store} from '@ngrx/store';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
import findKey from 'lodash/findKey';
|
import findKey from 'lodash/findKey';
|
||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
import union from 'lodash/union';
|
import union from 'lodash/union';
|
||||||
|
|
||||||
import { from as observableFrom, Observable, of as observableOf } from 'rxjs';
|
import {from as observableFrom, Observable, of as observableOf} from 'rxjs';
|
||||||
import { catchError, filter, map, mergeMap, switchMap, take, tap, withLatestFrom } from 'rxjs/operators';
|
import {catchError, filter, map, mergeMap, switchMap, take, tap, withLatestFrom} from 'rxjs/operators';
|
||||||
import { SubmissionObject } from '../../core/submission/models/submission-object.model';
|
import {SubmissionObject} from '../../core/submission/models/submission-object.model';
|
||||||
import { WorkflowItem } from '../../core/submission/models/workflowitem.model';
|
import {WorkflowItem} from '../../core/submission/models/workflowitem.model';
|
||||||
import { WorkspaceitemSectionUploadObject } from '../../core/submission/models/workspaceitem-section-upload.model';
|
import {WorkspaceitemSectionUploadObject} from '../../core/submission/models/workspaceitem-section-upload.model';
|
||||||
import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model';
|
import {WorkspaceitemSectionsObject} from '../../core/submission/models/workspaceitem-sections.model';
|
||||||
import { WorkspaceItem } from '../../core/submission/models/workspaceitem.model';
|
import {WorkspaceItem} from '../../core/submission/models/workspaceitem.model';
|
||||||
import { SubmissionJsonPatchOperationsService } from '../../core/submission/submission-json-patch-operations.service';
|
import {SubmissionJsonPatchOperationsService} from '../../core/submission/submission-json-patch-operations.service';
|
||||||
import { isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util';
|
import {isEmpty, isNotEmpty, isNotUndefined} from '../../shared/empty.util';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import {NotificationsService} from '../../shared/notifications/notifications.service';
|
||||||
import { SectionsType } from '../sections/sections-type';
|
import {SectionsType} from '../sections/sections-type';
|
||||||
import { SectionsService } from '../sections/sections.service';
|
import {SectionsService} from '../sections/sections.service';
|
||||||
import { SubmissionState } from '../submission.reducers';
|
import {SubmissionState} from '../submission.reducers';
|
||||||
import { SubmissionService } from '../submission.service';
|
import {SubmissionService} from '../submission.service';
|
||||||
import parseSectionErrors from '../utils/parseSectionErrors';
|
import parseSectionErrors from '../utils/parseSectionErrors';
|
||||||
import {
|
import {
|
||||||
CompleteInitSubmissionFormAction,
|
CompleteInitSubmissionFormAction,
|
||||||
@@ -43,18 +43,23 @@ import {
|
|||||||
SubmissionObjectAction,
|
SubmissionObjectAction,
|
||||||
SubmissionObjectActionTypes,
|
SubmissionObjectActionTypes,
|
||||||
UpdateSectionDataAction,
|
UpdateSectionDataAction,
|
||||||
UpdateSectionDataSuccessAction
|
UpdateSectionDataSuccessAction,
|
||||||
|
CleanDuplicateDetectionAction
|
||||||
} from './submission-objects.actions';
|
} from './submission-objects.actions';
|
||||||
import { SubmissionObjectEntry } from './submission-objects.reducer';
|
import {SubmissionObjectEntry} from './submission-objects.reducer';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import {Item} from '../../core/shared/item.model';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import {RemoteData} from '../../core/data/remote-data';
|
||||||
import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
import {getFirstSucceededRemoteDataPayload} from '../../core/shared/operators';
|
||||||
import { SubmissionObjectDataService } from '../../core/submission/submission-object-data.service';
|
import {SubmissionObjectDataService} from '../../core/submission/submission-object-data.service';
|
||||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
import {followLink} from '../../shared/utils/follow-link-config.model';
|
||||||
import parseSectionErrorPaths, { SectionErrorPath } from '../utils/parseSectionErrorPaths';
|
import parseSectionErrorPaths, {SectionErrorPath} from '../utils/parseSectionErrorPaths';
|
||||||
import { FormState } from '../../shared/form/form.reducer';
|
import {FormState} from '../../shared/form/form.reducer';
|
||||||
import { SubmissionSectionObject } from './submission-section-object.model';
|
import {SubmissionSectionObject} from './submission-section-object.model';
|
||||||
import { SubmissionSectionError } from './submission-section-error.model';
|
import {SubmissionSectionError} from './submission-section-error.model';
|
||||||
|
import {
|
||||||
|
WorkspaceitemSectionDuplicatesObject
|
||||||
|
} from '../../core/submission/models/workspaceitem-section-duplicates.model';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SubmissionObjectEffects {
|
export class SubmissionObjectEffects {
|
||||||
@@ -71,7 +76,12 @@ export class SubmissionObjectEffects {
|
|||||||
const selfLink = sectionDefinition._links.self.href || sectionDefinition._links.self;
|
const selfLink = sectionDefinition._links.self.href || sectionDefinition._links.self;
|
||||||
const sectionId = selfLink.substr(selfLink.lastIndexOf('/') + 1);
|
const sectionId = selfLink.substr(selfLink.lastIndexOf('/') + 1);
|
||||||
const config = sectionDefinition._links.config ? (sectionDefinition._links.config.href || sectionDefinition._links.config) : '';
|
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;
|
let sectionData;
|
||||||
if (sectionDefinition.sectionType !== SectionsType.SubmissionForm) {
|
if (sectionDefinition.sectionType !== SectionsType.SubmissionForm) {
|
||||||
sectionData = (isNotUndefined(action.payload.sections) && isNotUndefined(action.payload.sections[sectionId])) ? action.payload.sections[sectionId] : Object.create(null);
|
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])) {
|
&& isEmpty(sections[sherpaPoliciesSectionId])) {
|
||||||
mappedActions.push(new UpdateSectionDataAction(submissionId, sherpaPoliciesSectionId, null, [], []));
|
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;
|
return mappedActions;
|
||||||
}
|
}
|
||||||
@@ -481,3 +499,7 @@ function filterErrors(sectionForm: FormState, sectionErrors: SubmissionSectionEr
|
|||||||
});
|
});
|
||||||
return filteredErrors;
|
return filteredErrors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function alwaysDisplayDuplicates(): boolean {
|
||||||
|
return (environment.submission.duplicateDetection.alwaysShowSection);
|
||||||
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { submissionObjectReducer, SubmissionObjectState } from './submission-objects.reducer';
|
import { submissionObjectReducer, SubmissionObjectState } from './submission-objects.reducer';
|
||||||
import {
|
import {
|
||||||
CancelSubmissionFormAction,
|
CancelSubmissionFormAction,
|
||||||
ChangeSubmissionCollectionAction,
|
ChangeSubmissionCollectionAction, CleanDuplicateDetectionAction,
|
||||||
CompleteInitSubmissionFormAction,
|
CompleteInitSubmissionFormAction,
|
||||||
DeleteSectionErrorsAction,
|
DeleteSectionErrorsAction,
|
||||||
DeleteUploadedFileAction,
|
DeleteUploadedFileAction,
|
||||||
@@ -273,7 +273,7 @@ describe('submissionReducer test suite', () => {
|
|||||||
expect(newState[826].sections.traditionalpagetwo.enabled).toBeTruthy();
|
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 action: SubmissionObjectAction = new EnableSectionAction(submissionId, 'traditionalpagetwo');
|
||||||
let newState = submissionObjectReducer(initState, action);
|
let newState = submissionObjectReducer(initState, action);
|
||||||
@@ -644,4 +644,20 @@ describe('submissionReducer test suite', () => {
|
|||||||
expect(newState[826].sections.upload.data).toEqual(expectedState);
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -5,7 +5,7 @@ import isEqual from 'lodash/isEqual';
|
|||||||
import uniqWith from 'lodash/uniqWith';
|
import uniqWith from 'lodash/uniqWith';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChangeSubmissionCollectionAction,
|
ChangeSubmissionCollectionAction, CleanDuplicateDetectionAction,
|
||||||
CompleteInitSubmissionFormAction,
|
CompleteInitSubmissionFormAction,
|
||||||
DeleteSectionErrorsAction,
|
DeleteSectionErrorsAction,
|
||||||
DeleteUploadedFileAction,
|
DeleteUploadedFileAction,
|
||||||
@@ -229,6 +229,10 @@ export function submissionObjectReducer(state = initialState, action: Submission
|
|||||||
return removeSectionErrors(state, action as RemoveSectionErrorsAction);
|
return removeSectionErrors(state, action as RemoveSectionErrorsAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case SubmissionObjectActionTypes.CLEAN_DUPLICATE_DETECTION: {
|
||||||
|
return cleanDuplicateDetectionSection(state, action as CleanDuplicateDetectionAction);
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -856,3 +860,20 @@ function deleteFile(state: SubmissionObjectState, action: DeleteUploadedFileActi
|
|||||||
}
|
}
|
||||||
return state;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -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>
|
@@ -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 {
|
||||||
|
|
||||||
|
}
|
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@@ -9,5 +9,6 @@ export enum SectionsType {
|
|||||||
SherpaPolicies = 'sherpaPolicy',
|
SherpaPolicies = 'sherpaPolicy',
|
||||||
Identifiers = 'identifiers',
|
Identifiers = 'identifiers',
|
||||||
Collection = 'collection',
|
Collection = 'collection',
|
||||||
CoarNotify = 'coarnotify'
|
CoarNotify = 'coarnotify',
|
||||||
|
Duplicates = 'duplicates'
|
||||||
}
|
}
|
||||||
|
@@ -66,7 +66,8 @@ import {
|
|||||||
MetadataInformationComponent
|
MetadataInformationComponent
|
||||||
} from './sections/sherpa-policies/metadata-information/metadata-information.component';
|
} from './sections/sherpa-policies/metadata-information/metadata-information.component';
|
||||||
import { SectionFormOperationsService } from './sections/form/section-form-operations.service';
|
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 { SubmissionSectionCoarNotifyComponent } from './sections/section-coar-notify/section-coar-notify.component';
|
||||||
import {
|
import {
|
||||||
CoarNotifyConfigDataService
|
CoarNotifyConfigDataService
|
||||||
@@ -81,7 +82,8 @@ const ENTRY_COMPONENTS = [
|
|||||||
SubmissionSectionCcLicensesComponent,
|
SubmissionSectionCcLicensesComponent,
|
||||||
SubmissionSectionAccessesComponent,
|
SubmissionSectionAccessesComponent,
|
||||||
SubmissionSectionSherpaPoliciesComponent,
|
SubmissionSectionSherpaPoliciesComponent,
|
||||||
SubmissionSectionCoarNotifyComponent
|
SubmissionSectionCoarNotifyComponent,
|
||||||
|
SubmissionSectionDuplicatesComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
const DECLARATIONS = [
|
const DECLARATIONS = [
|
||||||
@@ -102,6 +104,7 @@ const DECLARATIONS = [
|
|||||||
SubmissionSectionUploadFileEditComponent,
|
SubmissionSectionUploadFileEditComponent,
|
||||||
SubmissionSectionUploadFileViewComponent,
|
SubmissionSectionUploadFileViewComponent,
|
||||||
SubmissionSectionIdentifiersComponent,
|
SubmissionSectionIdentifiersComponent,
|
||||||
|
SubmissionSectionDuplicatesComponent,
|
||||||
SubmissionImportExternalComponent,
|
SubmissionImportExternalComponent,
|
||||||
ThemedSubmissionImportExternalComponent,
|
ThemedSubmissionImportExternalComponent,
|
||||||
SubmissionImportExternalSearchbarComponent,
|
SubmissionImportExternalSearchbarComponent,
|
||||||
|
@@ -2826,6 +2826,8 @@
|
|||||||
|
|
||||||
"item.preview.organization.legalName": "Legal Name",
|
"item.preview.organization.legalName": "Legal Name",
|
||||||
|
|
||||||
|
"item.preview.dspace.entity.type": "Entity Type:",
|
||||||
|
|
||||||
"item.select.confirm": "Confirm selected",
|
"item.select.confirm": "Confirm selected",
|
||||||
|
|
||||||
"item.select.empty": "No items to show",
|
"item.select.empty": "No items to show",
|
||||||
@@ -5106,7 +5108,7 @@
|
|||||||
|
|
||||||
"submission.sections.submit.progressbar.describe.steptwo": "Describe",
|
"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",
|
"submission.sections.submit.progressbar.identifiers": "Identifiers",
|
||||||
|
|
||||||
@@ -5238,6 +5240,14 @@
|
|||||||
|
|
||||||
"submission.sections.accesses.form.until-placeholder": "Until",
|
"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.granted-label": "I confirm the license above",
|
||||||
|
|
||||||
"submission.sections.license.required": "You must accept the license",
|
"submission.sections.license.required": "You must accept the license",
|
||||||
@@ -5364,6 +5374,8 @@
|
|||||||
|
|
||||||
"submission.workflow.tasks.pool.show-detail": "Show detail",
|
"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": "View",
|
||||||
|
|
||||||
"submission.workspace.generic.view-help": "Select this option to view the item's metadata.",
|
"submission.workspace.generic.view-help": "Select this option to view the item's metadata.",
|
||||||
|
@@ -13,6 +13,7 @@ describe('Config Util', () => {
|
|||||||
expect(appConfig.ui.useProxies).toEqual(true);
|
expect(appConfig.ui.useProxies).toEqual(true);
|
||||||
|
|
||||||
expect(appConfig.submission.autosave.metadata).toEqual([]);
|
expect(appConfig.submission.autosave.metadata).toEqual([]);
|
||||||
|
expect(appConfig.submission.duplicateDetection.alwaysShowSection).toEqual(false);
|
||||||
|
|
||||||
expect(appConfig.themes.length).toEqual(1);
|
expect(appConfig.themes.length).toEqual(1);
|
||||||
expect(appConfig.themes[0].name).toEqual('dspace');
|
expect(appConfig.themes[0].name).toEqual('dspace');
|
||||||
|
@@ -157,6 +157,9 @@ export class DefaultAppConfig implements AppConfig {
|
|||||||
*/
|
*/
|
||||||
timer: 0
|
timer: 0
|
||||||
},
|
},
|
||||||
|
duplicateDetection: {
|
||||||
|
alwaysShowSection: false
|
||||||
|
},
|
||||||
typeBind: {
|
typeBind: {
|
||||||
field: 'dc.type'
|
field: 'dc.type'
|
||||||
},
|
},
|
||||||
|
@@ -5,6 +5,10 @@ interface AutosaveConfig extends Config {
|
|||||||
timer: number;
|
timer: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DuplicateDetectionConfig extends Config {
|
||||||
|
alwaysShowSection: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface TypeBindConfig extends Config {
|
interface TypeBindConfig extends Config {
|
||||||
field: string;
|
field: string;
|
||||||
}
|
}
|
||||||
@@ -29,6 +33,7 @@ export interface ConfidenceIconConfig extends Config {
|
|||||||
|
|
||||||
export interface SubmissionConfig extends Config {
|
export interface SubmissionConfig extends Config {
|
||||||
autosave: AutosaveConfig;
|
autosave: AutosaveConfig;
|
||||||
|
duplicateDetection: DuplicateDetectionConfig;
|
||||||
typeBind: TypeBindConfig;
|
typeBind: TypeBindConfig;
|
||||||
icons: IconsConfig;
|
icons: IconsConfig;
|
||||||
}
|
}
|
||||||
|
@@ -121,6 +121,9 @@ export const environment: BuildConfig = {
|
|||||||
// NOTE: every how many minutes submission is saved automatically
|
// NOTE: every how many minutes submission is saved automatically
|
||||||
timer: 5
|
timer: 5
|
||||||
},
|
},
|
||||||
|
duplicateDetection: {
|
||||||
|
alwaysShowSection: false
|
||||||
|
},
|
||||||
typeBind: {
|
typeBind: {
|
||||||
field: 'dc.type'
|
field: 'dc.type'
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user