Merge remote-tracking branch 'origin/main' into more-eslint

This commit is contained in:
Yury Bondarenko
2024-03-06 10:26:07 +01:00
896 changed files with 39886 additions and 9340 deletions

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

@@ -1,29 +1,8 @@
import {
ChangeDetectorRef,
Component,
Inject,
ViewChild,
} from '@angular/core';
import {
DynamicCheckboxModel,
DynamicFormControlEvent,
DynamicFormControlModel,
DynamicFormLayout,
} from '@ng-dynamic-forms/core';
import { ChangeDetectorRef, Component, Inject, ViewChild } from '@angular/core';
import { DynamicCheckboxModel, DynamicFormControlEvent, DynamicFormControlModel, DynamicFormLayout } from '@ng-dynamic-forms/core';
import { TranslateService } from '@ngx-translate/core';
import {
Observable,
Subscription,
} from 'rxjs';
import {
distinctUntilChanged,
filter,
find,
map,
mergeMap,
startWith,
take,
} from 'rxjs/operators';
import { Observable, Subscription } from 'rxjs';
import { distinctUntilChanged, filter, find, map, mergeMap, startWith, take } from 'rxjs/operators';
import { CollectionDataService } from '../../../core/data/collection-data.service';
import { RemoteData } from '../../../core/data/remote-data';
@@ -32,12 +11,7 @@ import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/jso
import { Collection } from '../../../core/shared/collection.model';
import { License } from '../../../core/shared/license.model';
import { WorkspaceitemSectionLicenseObject } from '../../../core/submission/models/workspaceitem-section-license.model';
import {
hasValue,
isNotEmpty,
isNotNull,
isNotUndefined,
} from '../../../shared/empty.util';
import { hasValue, isNotEmpty, isNotNull, isNotUndefined } from '../../../shared/empty.util';
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
import { FormComponent } from '../../../shared/form/form.component';
import { FormService } from '../../../shared/form/form.service';
@@ -49,10 +23,7 @@ import { SectionDataObject } from '../models/section-data.model';
import { SectionsService } from '../sections.service';
import { renderSectionFor } from '../sections-decorator';
import { SectionsType } from '../sections-type';
import {
SECTION_LICENSE_FORM_LAYOUT,
SECTION_LICENSE_FORM_MODEL,
} from './section-license.model';
import { SECTION_LICENSE_FORM_LAYOUT, SECTION_LICENSE_FORM_MODEL } from './section-license.model';
/**
* This component represents a section that contains the submission license form.
@@ -236,6 +207,7 @@ export class SubmissionSectionLicenseComponent extends SectionModelComponent {
} else {
this.operationsBuilder.remove(this.pathCombiner.getPath(path));
}
this.submissionService.dispatchSaveSection(this.submissionId, this.sectionData.id);
}
/**

View File

@@ -0,0 +1,92 @@
import { CoarNotifyConfigDataService } from './coar-notify-config-data.service';
import { testFindAllDataImplementation } from '../../../core/data/base/find-all-data.spec';
import { FindAllData } from '../../../core/data/base/find-all-data';
import { cold, getTestScheduler } from 'jasmine-marbles';
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { TestScheduler } from 'rxjs/testing';
import { RequestService } from '../../../core/data/request.service';
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { RequestEntry } from '../../../core/data/request-entry.model';
import { RemoteData } from '../../../core/data/remote-data';
import { RequestEntryState } from '../../../core/data/request-entry-state.model';
import { RestResponse } from '../../../core/cache/response.models';
import { of } from 'rxjs';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { testPatchDataImplementation } from '../../../core/data/base/patch-data.spec';
import { testDeleteDataImplementation } from '../../../core/data/base/delete-data.spec';
import { DeleteData } from '../../../core/data/base/delete-data';
import { PatchData } from '../../../core/data/base/patch-data';
import { CreateData } from '../../../core/data/base/create-data';
import { testCreateDataImplementation } from '../../../core/data/base/create-data.spec';
describe('CoarNotifyConfigDataService test', () => {
let scheduler: TestScheduler;
let service: CoarNotifyConfigDataService;
let requestService: RequestService;
let rdbService: RemoteDataBuildService;
let objectCache: ObjectCacheService;
let halService: HALEndpointService;
let notificationsService: NotificationsService;
let responseCacheEntry: RequestEntry;
const endpointURL = `https://rest.api/rest/api/coar-notify`;
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
const remoteDataMocks = {
Success: new RemoteData(null, null, null, RequestEntryState.Success, null, null, 200),
};
function initTestService() {
return new CoarNotifyConfigDataService(
requestService,
rdbService,
objectCache,
halService,
notificationsService
);
}
beforeEach(() => {
scheduler = getTestScheduler();
objectCache = {} as ObjectCacheService;
notificationsService = {} as NotificationsService;
responseCacheEntry = new RequestEntry();
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
requestService = jasmine.createSpyObj('requestService', {
generateRequestId: requestUUID,
send: true,
removeByHrefSubstring: {},
getByHref: of(responseCacheEntry),
getByUUID: of(responseCacheEntry),
});
halService = jasmine.createSpyObj('halService', {
getEndpoint: of(endpointURL)
});
rdbService = jasmine.createSpyObj('rdbService', {
buildSingle: createSuccessfulRemoteDataObject$({}, 500),
buildList: cold('a', { a: remoteDataMocks.Success })
});
service = initTestService();
});
describe('composition', () => {
const initCreateService = () => new CoarNotifyConfigDataService(null, null, null, null, null) as unknown as CreateData<any>;
const initFindAllService = () => new CoarNotifyConfigDataService(null, null, null, null, null) as unknown as FindAllData<any>;
const initDeleteService = () => new CoarNotifyConfigDataService(null, null, null, null, null) as unknown as DeleteData<any>;
const initPatchService = () => new CoarNotifyConfigDataService(null, null, null, null, null) as unknown as PatchData<any>;
testCreateDataImplementation(initCreateService);
testFindAllDataImplementation(initFindAllService);
testPatchDataImplementation(initPatchService);
testDeleteDataImplementation(initDeleteService);
});
});

View File

@@ -0,0 +1,113 @@
import { Injectable } from '@angular/core';
import { dataService } from '../../../core/data/base/data-service.decorator';
import { IdentifiableDataService } from '../../../core/data/base/identifiable-data.service';
import { FindAllData, FindAllDataImpl } from '../../../core/data/base/find-all-data';
import { DeleteData, DeleteDataImpl } from '../../../core/data/base/delete-data';
import { RequestService } from '../../../core/data/request.service';
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { FindListOptions } from '../../../core/data/find-list-options.model';
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
import { Observable } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { NoContent } from '../../../core/shared/NoContent.model';
import { map, take } from 'rxjs/operators';
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
import { MultipartPostRequest } from '../../../core/data/request.models';
import { RestRequest } from '../../../core/data/rest-request.model';
import { SUBMISSION_COAR_NOTIFY_CONFIG } from './section-coar-notify-service.resource-type';
import { SubmissionCoarNotifyConfig } from './submission-coar-notify.config';
import { CreateData, CreateDataImpl } from '../../../core/data/base/create-data';
import { PatchData, PatchDataImpl } from '../../../core/data/base/patch-data';
import { ChangeAnalyzer } from '../../../core/data/change-analyzer';
import { Operation } from 'fast-json-patch';
import { RestRequestMethod } from '../../../core/data/rest-request-method';
import { RequestParam } from '../../../core/cache/models/request-param.model';
/**
* A service responsible for fetching/sending data from/to the REST API on the CoarNotifyConfig endpoint
*/
@Injectable()
@dataService(SUBMISSION_COAR_NOTIFY_CONFIG)
export class CoarNotifyConfigDataService extends IdentifiableDataService<SubmissionCoarNotifyConfig> implements FindAllData<SubmissionCoarNotifyConfig>, DeleteData<SubmissionCoarNotifyConfig>, PatchData<SubmissionCoarNotifyConfig>, CreateData<SubmissionCoarNotifyConfig> {
createData: CreateDataImpl<SubmissionCoarNotifyConfig>;
private findAllData: FindAllDataImpl<SubmissionCoarNotifyConfig>;
private deleteData: DeleteDataImpl<SubmissionCoarNotifyConfig>;
private patchData: PatchDataImpl<SubmissionCoarNotifyConfig>;
private comparator: ChangeAnalyzer<SubmissionCoarNotifyConfig>;
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
) {
super('submissioncoarnotifyconfigs', requestService, rdbService, objectCache, halService);
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
this.patchData = new PatchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.comparator, this.responseMsToLive, this.constructIdEndpoint);
this.createData = new CreateDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive);
}
create(object: SubmissionCoarNotifyConfig, ...params: RequestParam[]): Observable<RemoteData<SubmissionCoarNotifyConfig>> {
return this.createData.create(object, ...params);
}
patch(object: SubmissionCoarNotifyConfig, operations: Operation[]): Observable<RemoteData<SubmissionCoarNotifyConfig>> {
return this.patchData.patch(object, operations);
}
update(object: SubmissionCoarNotifyConfig): Observable<RemoteData<SubmissionCoarNotifyConfig>> {
return this.patchData.update(object);
}
commitUpdates(method?: RestRequestMethod): void {
return this.patchData.commitUpdates(method);
}
createPatchFromCache(object: SubmissionCoarNotifyConfig): Observable<Operation[]> {
return this.patchData.createPatchFromCache(object);
}
findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<SubmissionCoarNotifyConfig>[]): Observable<RemoteData<PaginatedList<SubmissionCoarNotifyConfig>>> {
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
public delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
return this.deleteData.delete(objectId, copyVirtualMetadata);
}
public deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
return this.deleteData.deleteByHref(href, copyVirtualMetadata);
}
public invoke(serviceName: string, serviceId: string, files: File[]): Observable<RemoteData<SubmissionCoarNotifyConfig>> {
const requestId = this.requestService.generateRequestId();
this.getBrowseEndpoint().pipe(
take(1),
map((endpoint: string) => new URLCombiner(endpoint, serviceName, 'submissioncoarnotifyconfigmodel', serviceId).toString()),
map((endpoint: string) => {
const body = this.getInvocationFormData(files);
return new MultipartPostRequest(requestId, endpoint, body);
})
).subscribe((request: RestRequest) => this.requestService.send(request));
return this.rdbService.buildFromRequestUUID<SubmissionCoarNotifyConfig>(requestId);
}
private getInvocationFormData(files: File[]): FormData {
const form: FormData = new FormData();
files.forEach((file: File) => {
form.append('file', file);
});
return form;
}
}

View File

@@ -0,0 +1,13 @@
/**
* The resource type for Ldn-Services
*
* Needs to be in a separate file to prevent circular
* dependencies in webpack.
*/
import { ResourceType } from '../../../core/shared/resource-type';
export const SUBMISSION_COAR_NOTIFY_CONFIG = new ResourceType('submissioncoarnotifyconfig');
export const COAR_NOTIFY_WORKSPACEITEM = new ResourceType('workspaceitem');

View File

@@ -0,0 +1,149 @@
<div class="container-fluid">
<ng-container *ngIf="patterns?.length > 0 ">
<div *ngFor="let ldnPattern of patterns; let i = index" class="col">
<div *ngIf="(filterServices(ldnPattern.pattern ) | async)?.length > 0">
<label class="row col-form-label">
{{'submission.section.section-coar-notify.control.' + ldnPattern.pattern + '.label' | translate }}
</label
>
<div *ngIf="ldnServiceByPattern[ldnPattern.pattern]?.services.length">
<div
*ngFor="
let service of ldnServiceByPattern[ldnPattern.pattern].services;
let serviceIndex = index
"
>
<div class="row">
<div ngbDropdown #myDropdown="ngbDropdown" [class.mt-2]="serviceIndex > 0" class="flex-grow-1">
<div class="position-relative right-addon"
[attr.aria-label]="ldnPattern.pattern + '.dropdown'">
<i ngbDropdownToggle class="position-absolute scrollable-dropdown-toggle"
aria-hidden="true"></i>
<input
[attr.aria-label]="ldnPattern.pattern+'.dropdownanchor'"
type="text"
[readonly]="true"
[ngClass]="{'border-danger': (getShownSectionErrors$(ldnPattern.pattern, serviceIndex) | async)?.length > 0}"
class="form-control w-100 scrollable-dropdown-input"
[value]="ldnServiceByPattern[ldnPattern.pattern].services[serviceIndex]?.name"
(click)="myDropdown.open()"
/>
</div>
<div
ngbDropdownMenu
class="dropdown-menu scrollable-dropdown-menu w-100"
aria-haspopup="true"
aria-expanded="false"
>
<div
class="scrollable-menu"
role="listbox"
infiniteScroll
[infiniteScrollDistance]="2"
[infiniteScrollThrottle]="50"
[scrollWindow]="false"
>
<button
*ngIf="(filterServices(ldnPattern.pattern) | async)?.length == 0"
class="dropdown-item collection-item text-truncate w-100"
>
{{'submission.section.section-coar-notify.dropdown.no-data' | translate}}
</button>
<button
*ngIf="(filterServices(ldnPattern.pattern ) | async)?.length > 0"
class="dropdown-item collection-item text-truncate w-100"
(click)="onChange(ldnPattern.pattern, serviceIndex, null)"
>
{{'submission.section.section-coar-notify.dropdown.select-none' | translate}}
</button>
<button
*ngFor="let serviceOption of filterServices(ldnPattern.pattern ) | async"
[ngClass]="{'bg-light': ldnServiceByPattern[ldnPattern.pattern ].services[serviceIndex]?.id == serviceOption.id}"
class="dropdown-item collection-item text-truncate w-100"
(click)="onChange(ldnPattern.pattern, serviceIndex, serviceOption)"
>
<b>
{{ serviceOption.name }}
</b>
<br />
{{ serviceOption.description }}
</button>
</div>
</div>
</div>
<button *ngIf="ldnServiceByPattern[ldnPattern.pattern].services.length > 1"
type="button" [class.mt-2]="serviceIndex > 0"
class="btn btn-secondary ml-2"
role="button"
title="{{'form.remove' | translate}}"
[attr.aria-label]="'form.remove' | translate"
(click)="removeService(ldnPattern, serviceIndex)"
>
<span><i class="fas fa-trash" aria-hidden="true"></i></span>
</button>
</div>
<small
class="row text-muted"
*ngIf="!ldnServiceByPattern[ldnPattern.pattern].services[serviceIndex] &&
serviceIndex === ldnServiceByPattern[ldnPattern.pattern].services.length -1"
>
{{'submission.section.section-coar-notify.small.notification' | translate : {pattern : ldnPattern.pattern} }}
</small>
<ng-container *ngIf="(getShownSectionErrors$(ldnPattern.pattern , serviceIndex) | async)?.length > 0">
<small class="row text-danger" *ngFor="let error of (getShownSectionErrors$(ldnPattern.pattern , serviceIndex) | async)">
{{ error.message | translate}}
</small>
</ng-container>
<div
class="row mt-1"
*ngIf="ldnServiceByPattern[ldnPattern.pattern].services[serviceIndex]"
>
<div
class="alert alert-info w-100 d-flex align-items-center flex-row"
>
<i class="fa-solid fa-circle-info fa-xl ml-2"></i>
<div class="ml-4">
<div>{{ 'submission.section.section-coar-notify.selection.description' | translate }}</div>
<div *ngIf="ldnServiceByPattern[ldnPattern.pattern].services[serviceIndex]?.description; else noDesc">
{{ ldnServiceByPattern[ldnPattern.pattern].services[serviceIndex].description }}
</div>
<ng-template #noDesc>
<span class="text-muted">
{{ 'submission.section.section-coar-notify.selection.no-description' | translate }}
</span>
</ng-template>
</div>
</div>
</div>
<div class="row" *ngIf="(getShownSectionErrors$(ldnPattern.pattern, serviceIndex) | async)?.length > 0">
<div
class="alert alert-danger w-100 d-flex align-items-center flex-row"
>
<div class="ml-4">
<span>
{{ 'submission.section.section-coar-notify.notification.error' | translate }}
</span>
</div>
</div>
</div>
<div class="row">
<div *ngIf="ldnPattern.multipleRequest && (serviceIndex === ldnServiceByPattern[ldnPattern.pattern].services.length - 1)"
(click)="addNewService(ldnPattern)"
class="btn btn-link mt-2 pl-0"
>
<i class="fas fa-plus"></i>
{{ 'submission.sections.general.add-more' | translate }}
</div>
</div>
</div>
</div>
</div>
</div>
</ng-container>
<ng-container *ngIf="!hasSectionData">
<p>
{{ 'submission.section.section-coar-notify.info.no-pattern' | translate }}
</p>
</ng-container>
</div>

View File

@@ -0,0 +1,5 @@
// Getting styles for NgbDropdown
@import '../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.scss';
@import '../../../shared/form/form.component.scss';

View File

@@ -0,0 +1,440 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SubmissionSectionCoarNotifyComponent } from './section-coar-notify.component';
import { LdnServicesService } from '../../../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service';
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap';
import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder';
import { SectionsService } from '../sections.service';
import { CoarNotifyConfigDataService } from './coar-notify-config-data.service';
import { ChangeDetectorRef } from '@angular/core';
import { SubmissionCoarNotifyConfig } from './submission-coar-notify.config';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { createPaginatedList } from '../../../shared/testing/utils.test';
import { of } from 'rxjs';
import { LdnService, LdnServiceByPattern } from '../../../admin/admin-ldn-services/ldn-services-model/ldn-services.model';
import { NotifyServicePattern } from '../../../admin/admin-ldn-services/ldn-services-model/ldn-service-patterns.model';
import { TranslateModule } from '@ngx-translate/core';
describe('SubmissionSectionCoarNotifyComponent', () => {
let component: SubmissionSectionCoarNotifyComponent;
let componentAsAny: any;
let fixture: ComponentFixture<SubmissionSectionCoarNotifyComponent>;
let ldnServicesService: jasmine.SpyObj<LdnServicesService>;
let coarNotifyConfigDataService: jasmine.SpyObj<CoarNotifyConfigDataService>;
let operationsBuilder: jasmine.SpyObj<JsonPatchOperationsBuilder>;
let sectionService: jasmine.SpyObj<SectionsService>;
let cdRefStub: any;
const patterns: SubmissionCoarNotifyConfig[] = Object.assign(
[new SubmissionCoarNotifyConfig()],
{
patterns: [{pattern: 'review', multipleRequest: false}, {pattern: 'endorsment', multipleRequest: false}],
}
);
const patternsPL = createPaginatedList(patterns);
const coarNotifyConfig = createSuccessfulRemoteDataObject$(patternsPL);
beforeEach(async () => {
ldnServicesService = jasmine.createSpyObj('LdnServicesService', [
'findByInboundPattern',
]);
coarNotifyConfigDataService = jasmine.createSpyObj(
'CoarNotifyConfigDataService',
['findAll']
);
operationsBuilder = jasmine.createSpyObj('JsonPatchOperationsBuilder', [
'remove',
'replace',
'add',
'flushOperation',
]);
sectionService = jasmine.createSpyObj('SectionsService', [
'dispatchRemoveSectionErrors',
'getSectionServerErrors',
'setSectionError',
]);
cdRefStub = Object.assign({
detectChanges: () => fixture.detectChanges(),
});
await TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [SubmissionSectionCoarNotifyComponent],
providers: [
{ provide: LdnServicesService, useValue: ldnServicesService },
{ provide: CoarNotifyConfigDataService, useValue: coarNotifyConfigDataService},
{ provide: JsonPatchOperationsBuilder, useValue: operationsBuilder },
{ provide: SectionsService, useValue: sectionService },
{ provide: ChangeDetectorRef, useValue: cdRefStub },
{ provide: 'collectionIdProvider', useValue: 'collectionId' },
{ provide: 'sectionDataProvider', useValue: { id: 'sectionId', data: {} }},
{ provide: 'submissionIdProvider', useValue: 'submissionId' },
NgbDropdown,
],
}).compileComponents();
fixture = TestBed.createComponent(SubmissionSectionCoarNotifyComponent);
component = fixture.componentInstance;
componentAsAny = component;
component.patterns = patterns[0].patterns;
coarNotifyConfigDataService.findAll.and.returnValue(coarNotifyConfig);
sectionService.getSectionServerErrors.and.returnValue(
of(
Object.assign([], {
path: 'sections/sectionId/data/notifyCoar',
message: 'error',
})
)
);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('onSectionInit', () => {
it('should call setCoarNotifyConfig and getSectionServerErrorsAndSetErrorsToDisplay', () => {
spyOn(component, 'setCoarNotifyConfig');
spyOn(componentAsAny, 'getSectionServerErrorsAndSetErrorsToDisplay');
component.onSectionInit();
expect(component.setCoarNotifyConfig).toHaveBeenCalled();
expect(componentAsAny.getSectionServerErrorsAndSetErrorsToDisplay).toHaveBeenCalled();
});
});
describe('onChange', () => {
const ldnPattern = {pattern: 'review', multipleRequest: false};
const index = 0;
const selectedService: LdnService = Object.assign(new LdnService(), {
id: 1,
name: 'service1',
notifyServiceInboundPatterns: [
{
pattern: 'review',
},
],
description: '',
});
beforeEach(() => {
component.ldnServiceByPattern[ldnPattern.pattern] = {
allowsMultipleRequests: false,
services: []
} as LdnServiceByPattern;
component.patterns = [];
});
it('should do nothing if the selected value is the same as the previous one', () => {
component.ldnServiceByPattern[ldnPattern.pattern].services[index] = selectedService;
component.onChange(ldnPattern.pattern, index, selectedService);
expect(componentAsAny.operationsBuilder.remove).not.toHaveBeenCalled();
expect(componentAsAny.operationsBuilder.replace).not.toHaveBeenCalled();
expect(componentAsAny.operationsBuilder.add).not.toHaveBeenCalled();
});
it('should remove the path when the selected value is null', () => {
component.ldnServiceByPattern[ldnPattern.pattern].services[index] = selectedService;
component.onChange(ldnPattern.pattern, index, null);
expect(componentAsAny.operationsBuilder.flushOperation).toHaveBeenCalledWith(
componentAsAny.pathCombiner.getPath([ldnPattern.pattern, '-'])
);
expect(component.ldnServiceByPattern[ldnPattern.pattern].services[index]).toBeNull();
expect(component.previousServices[ldnPattern.pattern].services[index]).toBeNull();
});
it('should replace the path when there is a previous value stored and it is different from the new one', () => {
const previousService: LdnService = Object.assign(new LdnService(), {
id: 2,
name: 'service2',
notifyServiceInboundPatterns: [
{
pattern: 'endorsement',
},
],
description: 'test',
});
component.ldnServiceByPattern[ldnPattern.pattern].services[index] = previousService;
component.previousServices[ldnPattern.pattern] = {
allowsMultipleRequests: false,
services: [previousService]
} as LdnServiceByPattern;
component.onChange(ldnPattern.pattern, index, selectedService);
expect(componentAsAny.operationsBuilder.add).toHaveBeenCalledWith(
componentAsAny.pathCombiner.getPath([ldnPattern.pattern, '-']),
[selectedService.id],
false,
true
);
expect(component.ldnServiceByPattern[ldnPattern.pattern].services[index]).toEqual(
selectedService
);
expect(component.previousServices[ldnPattern.pattern].services[index].id).toEqual(
selectedService.id
);
});
it('should add the path when there is no previous value stored', () => {
component.onChange(ldnPattern.pattern, index, selectedService);
expect(componentAsAny.operationsBuilder.add).toHaveBeenCalledWith(
componentAsAny.pathCombiner.getPath([ldnPattern.pattern, '-']),
[selectedService.id],
false,
true
);
expect(component.ldnServiceByPattern[ldnPattern.pattern].services[index]).toEqual(
selectedService
);
expect(component.previousServices[ldnPattern.pattern].services[index].id).toEqual(
selectedService.id
);
});
});
describe('initSelectedServicesByPattern', () => {
const pattern1 = {pattern: 'review', multipleRequest: false};
const pattern2 = {pattern: 'endorsement', multipleRequest: false};
const service1: LdnService = Object.assign(new LdnService(), {
id: 1,
uuid: 1,
name: 'service1',
notifyServiceInboundPatterns: [
Object.assign(new NotifyServicePattern(), {
pattern: pattern1,
}),
],
});
const service2: LdnService = Object.assign(new LdnService(), {
id: 2,
uuid: 2,
name: 'service2',
notifyServiceInboundPatterns: [
Object.assign(new NotifyServicePattern(), {
pattern: pattern2,
}),
],
});
const service3: LdnService = Object.assign(new LdnService(), {
id: 3,
uuid: 3,
name: 'service3',
notifyServiceInboundPatterns: [
Object.assign(new NotifyServicePattern(), {
pattern: pattern1,
}),
Object.assign(new NotifyServicePattern(), {
pattern: pattern2,
}),
],
});
const services = [service1, service2, service3];
beforeEach(() => {
ldnServicesService.findByInboundPattern.and.returnValue(
createSuccessfulRemoteDataObject$(createPaginatedList(services))
);
component.ldnServiceByPattern[pattern1.pattern] = {
allowsMultipleRequests: false,
services: []
} as LdnServiceByPattern;
component.ldnServiceByPattern[pattern2.pattern] = {
allowsMultipleRequests: false,
services: []
} as LdnServiceByPattern;
component.patterns = [pattern1, pattern2];
spyOn(component, 'filterServices').and.callFake((pattern) => {
return of(services);
});
});
it('should initialize the selected services by pattern', () => {
component.initSelectedServicesByPattern();
expect(component.ldnServiceByPattern[pattern1.pattern].services).toEqual([null]);
expect(component.ldnServiceByPattern[pattern2.pattern].services).toEqual([null]);
});
it('should add the service to the selected services by pattern if the section data has a value for the pattern', () => {
component.sectionData.data[pattern1.pattern] = [service1.uuid, service3.uuid];
component.sectionData.data[pattern2.pattern] = [service2.uuid, service3.uuid];
component.initSelectedServicesByPattern();
expect(component.ldnServiceByPattern[pattern1.pattern].services).toEqual([
service1,
service3,
]);
expect(component.ldnServiceByPattern[pattern2.pattern].services).toEqual([
service2,
service3,
]);
});
});
describe('addService', () => {
const ldnPattern = {pattern: 'review', multipleRequest: false};
const service: any = {
id: 1,
name: 'service1',
notifyServiceInboundPatterns: [{ pattern: ldnPattern.pattern }],
};
beforeEach(() => {
component.ldnServiceByPattern[ldnPattern.pattern] = {
allowsMultipleRequests: false,
services: []
} as LdnServiceByPattern;
});
it('should push the new service to the array corresponding to the pattern', () => {
component.addService(ldnPattern, service);
expect(component.ldnServiceByPattern[ldnPattern.pattern].services).toEqual([service]);
});
});
describe('removeService', () => {
const ldnPattern = {pattern: 'review', multipleRequest: false};
const service1: LdnService = Object.assign(new LdnService(), {
id: 1,
name: 'service1',
notifyServiceInboundPatterns: [
Object.assign(new NotifyServicePattern(), {
pattern: ldnPattern.pattern,
}),
],
});
const service2: LdnService = Object.assign(new LdnService(), {
id: 2,
name: 'service2',
notifyServiceInboundPatterns: [
Object.assign(new NotifyServicePattern(), {
pattern: ldnPattern.pattern,
}),
],
});
const service3: LdnService = Object.assign(new LdnService(), {
id: 3,
name: 'service3',
notifyServiceInboundPatterns: [
Object.assign(new NotifyServicePattern(), {
pattern: ldnPattern.pattern,
}),
],
});
beforeEach(() => {
component.ldnServiceByPattern[ldnPattern.pattern] = {
allowsMultipleRequests: false,
services: []
} as LdnServiceByPattern;
});
it('should remove the service at the specified index from the array corresponding to the pattern', () => {
component.ldnServiceByPattern[ldnPattern.pattern].services = [service1, service2, service3];
component.removeService(ldnPattern, 1);
expect(component.ldnServiceByPattern[ldnPattern.pattern].services).toEqual([
service1,
service3,
]);
});
});
describe('filterServices', () => {
const pattern = 'review';
const service1: any = {
id: 1,
name: 'service1',
notifyServiceInboundPatterns: [{ pattern: pattern }],
};
const service2: any = {
id: 2,
name: 'service2',
notifyServiceInboundPatterns: [{ pattern: pattern }],
};
const service3: any = {
id: 3,
name: 'service3',
notifyServiceInboundPatterns: [{ pattern: pattern }],
};
const services = [service1, service2, service3];
beforeEach(() => {
ldnServicesService.findByInboundPattern.and.returnValue(
createSuccessfulRemoteDataObject$(createPaginatedList(services))
);
});
it('should return an observable of the services that match the given pattern', () => {
component.filterServices(pattern).subscribe((result) => {
expect(result).toEqual(services);
});
});
});
describe('hasInboundPattern', () => {
const pattern = 'review';
const service: any = {
id: 1,
name: 'service1',
notifyServiceInboundPatterns: [{ pattern: pattern }],
};
it('should return true if the service has the specified inbound pattern type', () => {
expect(component.hasInboundPattern(service, pattern)).toBeTrue();
});
it('should return false if the service does not have the specified inbound pattern type', () => {
expect(component.hasInboundPattern(service, 'endorsement')).toBeFalse();
});
});
describe('getSectionServerErrorsAndSetErrorsToDisplay', () => {
it('should set the validation errors for the current section to display', () => {
const validationErrors = [
{ path: 'sections/sectionId/data/notifyCoar', message: 'error' },
];
sectionService.getSectionServerErrors.and.returnValue(
of(validationErrors)
);
componentAsAny.getSectionServerErrorsAndSetErrorsToDisplay();
expect(sectionService.setSectionError).toHaveBeenCalledWith(
component.submissionId,
component.sectionData.id,
validationErrors[0]
);
});
});
describe('onSectionDestroy', () => {
it('should unsubscribe from all subscriptions', () => {
const sub1 = of(null).subscribe();
const sub2 = of(null).subscribe();
componentAsAny.subs = [sub1, sub2];
spyOn(sub1, 'unsubscribe');
spyOn(sub2, 'unsubscribe');
component.onSectionDestroy();
expect(sub1.unsubscribe).toHaveBeenCalled();
expect(sub2.unsubscribe).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,337 @@
import { ChangeDetectorRef, Component, Inject } from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import { SectionModelComponent } from '../models/section.model';
import { renderSectionFor } from '../sections-decorator';
import { SectionsType } from '../sections-type';
import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner';
import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder';
import { SectionsService } from '../sections.service';
import { SectionDataObject } from '../models/section-data.model';
import { hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
import { getFirstCompletedRemoteData, getPaginatedListPayload, getRemoteDataPayload } from '../../../core/shared/operators';
import { LdnServicesService } from '../../../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service';
import { LdnService, LdnServiceByPattern } from '../../../admin/admin-ldn-services/ldn-services-model/ldn-services.model';
import { CoarNotifyConfigDataService } from './coar-notify-config-data.service';
import { filter, map, take, tap } from 'rxjs/operators';
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap';
import { SubmissionSectionError } from '../../objects/submission-section-error.model';
import { LdnPattern } from './submission-coar-notify.config';
/**
* This component represents a section that contains the submission section-coar-notify form.
*/
@Component({
selector: 'ds-submission-section-coar-notify',
templateUrl: './section-coar-notify.component.html',
styleUrls: ['./section-coar-notify.component.scss'],
providers: [NgbDropdown]
})
@renderSectionFor(SectionsType.CoarNotify)
export class SubmissionSectionCoarNotifyComponent extends SectionModelComponent {
hasSectionData = false;
/**
* Contains an array of string patterns.
*/
patterns: LdnPattern[] = [];
/**
* An object that maps string keys to arrays of LdnService objects.
* Used to store LdnService objects by pattern.
*/
ldnServiceByPattern: { [key: string]: LdnServiceByPattern } = {};
/**
* A map representing all services for each pattern
* {
* 'pattern': {
* 'index': 'service.id'
* }
* }
*
* @type {{ [key: string]: {[key: number]: number} }}
* @memberof SubmissionSectionCoarNotifyComponent
*/
previousServices: { [key: string]: LdnServiceByPattern } = {};
/**
* The [[JsonPatchOperationPathCombiner]] object
* @type {JsonPatchOperationPathCombiner}
*/
protected pathCombiner: JsonPatchOperationPathCombiner;
/**
* A map representing all field on their way to be removed
* @type {Map}
*/
protected fieldsOnTheirWayToBeRemoved: Map<string, number[]> = new Map();
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
protected subs: Subscription[] = [];
private filteredServicesByPattern = {};
constructor(protected ldnServicesService: LdnServicesService,
// protected formOperationsService: SectionFormOperationsService,
protected operationsBuilder: JsonPatchOperationsBuilder,
protected sectionService: SectionsService,
protected coarNotifyConfigDataService: CoarNotifyConfigDataService,
protected chd: ChangeDetectorRef,
@Inject('collectionIdProvider') public injectedCollectionId: string,
@Inject('sectionDataProvider') public injectedSectionData: SectionDataObject,
@Inject('submissionIdProvider') public injectedSubmissionId: string) {
super(injectedCollectionId, injectedSectionData, injectedSubmissionId);
}
/**
* Initialize all instance variables
*/
onSectionInit() {
this.setCoarNotifyConfig();
this.getSectionServerErrorsAndSetErrorsToDisplay();
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionData.id);
}
/**
* Method called when section is initialized
* Retriev available NotifyConfigs
*/
setCoarNotifyConfig() {
this.subs.push(
this.coarNotifyConfigDataService.findAll().pipe(
getFirstCompletedRemoteData()
).subscribe((data) => {
if (data.hasSucceeded) {
this.patterns = data.payload.page[0].patterns;
this.initSelectedServicesByPattern();
}
}));
}
/**
* Handles the change event of a select element.
* @param pattern - The pattern of the select element.
* @param index - The index of the select element.
* @param selectedService - The selected LDN service.
*/
onChange(pattern: string, index: number, selectedService: LdnService | null) {
// do nothing if the selected value is the same as the previous one
if (this.ldnServiceByPattern[pattern].services[index]?.id === selectedService?.id) {
return;
}
// initialize the previousServices object for the pattern if it does not exist
if (!this.previousServices[pattern]) {
this.previousServices[pattern] = {
services: [],
allowsMultipleRequests: this.patterns.find(ldnPattern => ldnPattern.pattern === pattern)?.multipleRequest
};
}
// store the previous value
this.previousServices[pattern].services[index] = this.ldnServiceByPattern[pattern].services[index];
// set the new value
this.ldnServiceByPattern[pattern].services[index] = selectedService;
const hasPrevValueStored = hasValue(this.previousServices[pattern].services[index]) && this.previousServices[pattern].services[index].id !== selectedService?.id;
if (hasPrevValueStored) {
// when there is a previous value stored and it is different from the new one
this.operationsBuilder.flushOperation(this.pathCombiner.getPath([pattern, '-']));
if (this.filteredServicesByPattern[pattern]?.includes(this.previousServices[pattern].services[index])){
this.operationsBuilder.remove(this.pathCombiner.getPath([pattern, index.toString()]));
}
}
if (!hasPrevValueStored || (selectedService?.id && hasPrevValueStored)) {
// add the path when there is no previous value stored
this.operationsBuilder.add(this.pathCombiner.getPath([pattern, '-']), [selectedService.id], false, true);
}
// set the previous value to the new value
this.previousServices[pattern].services[index] = this.ldnServiceByPattern[pattern].services[index];
this.sectionService.dispatchRemoveSectionErrors(this.submissionId, this.sectionData.id);
this.chd.detectChanges();
}
/**
* Initializes the selected services by pattern.
* Loops through each pattern and filters the services based on the pattern.
* If the section data has a value for the pattern, it adds the service to the selected services by pattern.
* If the section data does not have a value for the pattern, it adds a null service to the selected services by pattern,
* so that the select element is initialized with a null value and to display the default select input.
*/
initSelectedServicesByPattern(): void {
this.patterns.forEach((ldnPattern) => {
if (hasValue(this.sectionData.data[ldnPattern.pattern])) {
this.subs.push(
this.filterServices(ldnPattern.pattern)
.subscribe((services: LdnService[]) => {
if (!this.ldnServiceByPattern[ldnPattern.pattern]) {
this.ldnServiceByPattern[ldnPattern.pattern] = {
services: [],
allowsMultipleRequests: ldnPattern.multipleRequest
};
}
this.ldnServiceByPattern[ldnPattern.pattern].services = services.filter((service) => {
const selection = (this.sectionData.data[ldnPattern.pattern] as LdnService[]).find((s: LdnService) => s.id === service.id);
this.addService(ldnPattern, selection);
return this.sectionData.data[ldnPattern.pattern].includes(service.uuid);
});
})
);
} else {
this.ldnServiceByPattern[ldnPattern.pattern] = {
services: [],
allowsMultipleRequests: ldnPattern.multipleRequest
};
this.addService(ldnPattern, null);
}
});
}
/**
* Adds a new service to the selected services for the given pattern.
* @param ldnPattern - The pattern to add the new service to.
* @param newService - The new service to add.
*/
addService(ldnPattern: LdnPattern, newService: LdnService) {
// Your logic to add a new service to the selected services for the pattern
// Example: Push the newService to the array corresponding to the pattern
if (!this.ldnServiceByPattern[ldnPattern.pattern]) {
this.ldnServiceByPattern[ldnPattern.pattern] = {
services: [],
allowsMultipleRequests: ldnPattern.multipleRequest
};
}
this.ldnServiceByPattern[ldnPattern.pattern].services.push(newService);
}
/**
* Removes the service at the specified index from the array corresponding to the pattern.
* @param ldnPattern - The LDN pattern from which to remove the service
* @param serviceIndex - the service index to remove
*/
removeService(ldnPattern: LdnPattern, serviceIndex: number) {
if (this.ldnServiceByPattern[ldnPattern.pattern]) {
// Remove the service at the specified index from the array
this.ldnServiceByPattern[ldnPattern.pattern].services.splice(serviceIndex, 1);
this.previousServices[ldnPattern.pattern]?.services.splice(serviceIndex, 1);
this.operationsBuilder.flushOperation(this.pathCombiner.getPath([ldnPattern.pattern, '-']));
this.sectionService.dispatchRemoveSectionErrors(this.submissionId, this.sectionData.id);
}
if (!this.ldnServiceByPattern[ldnPattern.pattern].services.length) {
this.addNewService(ldnPattern);
}
}
/**
* Method called when dropdowns for the section are initialized
* Retrieve services with corresponding patterns to the dropdowns.
*/
filterServices(pattern: string): Observable<LdnService[]> {
return this.ldnServicesService.findByInboundPattern(pattern).pipe(
getFirstCompletedRemoteData(),
tap((rd) => {
if (rd.hasFailed) {
throw new Error(`Failed to retrieve services for pattern ${pattern}`);
}
}),
filter((rd) => rd.hasSucceeded),
getRemoteDataPayload(),
getPaginatedListPayload(),
tap(res => {
if (!this.filteredServicesByPattern[pattern]){
this.filteredServicesByPattern[pattern] = [];
}
if (this.filteredServicesByPattern[pattern].length === 0) {
this.filteredServicesByPattern[pattern].push(...res);
}
}),
map((res: LdnService[]) => res.filter((service) => {
if (!this.hasSectionData){
this.hasSectionData = this.hasInboundPattern(service, pattern);
}
return this.hasInboundPattern(service, pattern);
}))
);
}
/**
* Checks if the given service has the specified inbound pattern type.
* @param service - The service to check.
* @param patternType - The inbound pattern type to look for.
* @returns True if the service has the specified inbound pattern type, false otherwise.
*/
hasInboundPattern(service: any, patternType: string): boolean {
return service.notifyServiceInboundPatterns.some((pattern: { pattern: string }) => {
return pattern.pattern === patternType;
});
}
/**
* Retrieves server errors for the current section and sets them to display.
* @returns An Observable that emits the validation errors for the current section.
*/
private getSectionServerErrorsAndSetErrorsToDisplay() {
this.subs.push(
this.sectionService.getSectionServerErrors(this.submissionId, this.sectionData.id).pipe(
take(1),
filter((validationErrors) => isNotEmpty(validationErrors)),
).subscribe((validationErrors: SubmissionSectionError[]) => {
if (isNotEmpty(validationErrors)) {
validationErrors.forEach((error) => {
this.sectionService.setSectionError(this.submissionId, this.sectionData.id, error);
});
}
}));
}
/**
* Returns an observable of the errors for the current section that match the given pattern and index.
* @param pattern - The pattern to match against the error paths.
* @param index - The index to match against the error paths.
* @returns An observable of the errors for the current section that match the given pattern and index.
*/
public getShownSectionErrors$(pattern: string, index: number): Observable<SubmissionSectionError[]> {
return this.sectionService.getShownSectionErrors(this.submissionId, this.sectionData.id, this.sectionData.sectionType)
.pipe(
take(1),
filter((validationErrors) => isNotEmpty(validationErrors)),
map((validationErrors: SubmissionSectionError[]) => {
return validationErrors.filter((error) => {
const path = `${pattern}/${index}`;
return error.path.includes(path);
});
})
);
}
/**
* @returns An observable that emits a boolean indicating whether the section has any server errors or not.
*/
protected getSectionStatus(): Observable<boolean> {
return this.sectionService.getSectionServerErrors(this.submissionId, this.sectionData.id).pipe(
map((validationErrors) => isEmpty(validationErrors)
));
}
/**
* Unsubscribe from all subscriptions
*/
onSectionDestroy() {
this.subs
.filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe());
}
/**
* Add new row to dropdown for multiple service selection
* @param ldnPattern - the related LDN pattern where the service is added
*/
addNewService(ldnPattern: LdnPattern): void {
//idle new service for new selection
this.ldnServiceByPattern[ldnPattern.pattern].services.push(null);
}
}

View File

@@ -0,0 +1,34 @@
import { CacheableObject } from '../../../core/cache/cacheable-object.model';
import { autoserialize, deserialize, deserializeAs, inheritSerialization } from 'cerialize';
import { excludeFromEquals } from '../../../core/utilities/equals.decorators';
import { typedObject } from '../../../core/cache/builders/build-decorators';
import { COAR_NOTIFY_WORKSPACEITEM } from './section-coar-notify-service.resource-type';
/** An CoarNotify and its properties. */
@typedObject
@inheritSerialization(CacheableObject)
export class SubmissionCoarNotifyWorkspaceitemModel extends CacheableObject {
static type = COAR_NOTIFY_WORKSPACEITEM;
@excludeFromEquals
@autoserialize
endorsement?: number[];
@deserializeAs('id')
review?: number[];
@autoserialize
ingest?: number[];
@deserialize
_links: {
self: {
href: string;
};
};
get self(): string {
return this._links.self.href;
}
}

View File

@@ -0,0 +1,42 @@
import { ResourceType } from '../../../core/shared/resource-type';
import { CacheableObject } from '../../../core/cache/cacheable-object.model';
import { autoserialize, deserialize, deserializeAs, inheritSerialization } from 'cerialize';
import { excludeFromEquals } from '../../../core/utilities/equals.decorators';
import { typedObject } from '../../../core/cache/builders/build-decorators';
import { SUBMISSION_COAR_NOTIFY_CONFIG } from './section-coar-notify-service.resource-type';
export interface LdnPattern {
pattern: string,
multipleRequest: boolean
}
/** A SubmissionCoarNotifyConfig and its properties. */
@typedObject
@inheritSerialization(CacheableObject)
export class SubmissionCoarNotifyConfig extends CacheableObject {
static type = SUBMISSION_COAR_NOTIFY_CONFIG;
@excludeFromEquals
@autoserialize
type: ResourceType;
@autoserialize
id: string;
@deserializeAs('id')
uuid: string;
@autoserialize
patterns: LdnPattern[];
@deserialize
_links: {
self: {
href: string;
};
};
get self(): string {
return this._links.self.href;
}
}

View File

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

View File

@@ -6,7 +6,6 @@
</button>
</div>
<div class="modal-body">
<ds-form *ngIf="formModel"
#formRef="formComponent"
[formId]="formId"

View File

@@ -1,32 +1,10 @@
import { CommonModule } from '@angular/common';
import {
ChangeDetectorRef,
Component,
NO_ERRORS_SCHEMA,
} from '@angular/core';
import {
ComponentFixture,
fakeAsync,
inject,
TestBed,
tick,
waitForAsync,
} from '@angular/core/testing';
import {
FormsModule,
ReactiveFormsModule,
} from '@angular/forms';
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import {
NgbActiveModal,
NgbModal,
} from '@ng-bootstrap/ng-bootstrap';
import {
DynamicFormArrayModel,
DynamicFormControlEvent,
DynamicFormGroupModel,
DynamicSelectModel,
} from '@ng-dynamic-forms/core';
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { DynamicFormArrayModel, DynamicFormControlEvent, DynamicFormGroupModel, DynamicSelectModel } from '@ng-dynamic-forms/core';
import { TranslateModule } from '@ngx-translate/core';
import { of } from 'rxjs';
@@ -40,21 +18,14 @@ import { FormComponent } from '../../../../../shared/form/form.component';
import { FormService } from '../../../../../shared/form/form.service';
import { getMockFormService } from '../../../../../shared/mocks/form-service.mock';
import { getMockSectionUploadService } from '../../../../../shared/mocks/section-upload.service.mock';
import {
mockFileFormData,
mockSubmissionCollectionId,
mockSubmissionId,
mockSubmissionObject,
mockUploadConfigResponse,
mockUploadConfigResponseMetadata,
mockUploadFiles,
} from '../../../../../shared/mocks/submission.mock';
import { mockFileFormData, mockSubmissionCollectionId, mockSubmissionId, mockSubmissionObject, mockUploadConfigResponse, mockUploadConfigResponseMetadata, mockUploadFiles } from '../../../../../shared/mocks/submission.mock';
import { SubmissionJsonPatchOperationsServiceStub } from '../../../../../shared/testing/submission-json-patch-operations-service.stub';
import { SubmissionServiceStub } from '../../../../../shared/testing/submission-service.stub';
import { createTestComponent } from '../../../../../shared/testing/utils.test';
import { SubmissionService } from '../../../../submission.service';
import { POLICY_DEFAULT_WITH_LIST } from '../../section-upload.component';
import { SectionUploadService } from '../../section-upload.service';
import { DynamicCustomSwitchModel } from '../../../../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model';
import { SubmissionSectionUploadFileEditComponent } from './section-upload-file-edit.component';
const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', {
@@ -87,7 +58,10 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
const fileIndex = '0';
const fileId = '123456-test-upload';
const fileData: any = mockUploadFiles[0];
const pathCombiner = new JsonPatchOperationPathCombiner('sections', sectionId, 'files', fileIndex);
const pathCombiner = new JsonPatchOperationPathCombiner('sections', sectionId);
let noAccessConditionsMock = Object.assign({}, mockFileFormData);
delete noAccessConditionsMock.accessConditions;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
@@ -192,11 +166,15 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
comp.ngOnInit();
const models = [DynamicCustomSwitchModel, DynamicFormGroupModel, DynamicFormArrayModel];
expect(comp.formModel).toBeDefined();
expect(comp.formModel.length).toBe(2);
expect(comp.formModel[0] instanceof DynamicFormGroupModel).toBeTruthy();
expect(comp.formModel[1] instanceof DynamicFormArrayModel).toBeTruthy();
expect((comp.formModel[1] as DynamicFormArrayModel).groups.length).toBe(2);
expect(comp.formModel.length).toBe(models.length);
models.forEach((model, i) => {
expect(comp.formModel[i] instanceof model).toBeTruthy();
});
expect((comp.formModel[2] as DynamicFormArrayModel).groups.length).toBe(2);
const startDateModel = formbuilderService.findById('startDate', comp.formModel);
expect(startDateModel.max).toEqual(maxStartDate);
const endDateModel = formbuilderService.findById('endDate', comp.formModel);
@@ -260,6 +238,7 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
compAsAny.formRef = { formGroup: null };
compAsAny.fileData = fileData;
compAsAny.pathCombiner = pathCombiner;
compAsAny.isPrimary = null;
formService.validateAllFormFields.and.callFake(() => null);
formService.isValid.and.returnValue(of(true));
formService.getFormData.and.returnValue(of(mockFileFormData));
@@ -268,10 +247,11 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
Object.assign(mockSubmissionObject, {
sections: {
upload: {
files: mockUploadFiles,
},
},
}),
primary: true,
files: mockUploadFiles
}
}
})
];
operationsService.jsonPatchByResourceID.and.returnValue(of(response));
@@ -283,23 +263,28 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
comp.saveBitstreamData();
tick();
let path = 'metadata/dc.title';
let path = 'primary';
expect(uploadService.updatePrimaryBitstreamOperation).toHaveBeenCalledWith(pathCombiner.getPath(path), compAsAny.isPrimary, mockFileFormData.primary[0], compAsAny.fileId);
const pathFragment = ['files', fileIndex];
path = 'metadata/dc.title';
expect(operationsBuilder.add).toHaveBeenCalledWith(
pathCombiner.getPath(path),
pathCombiner.getPath([...pathFragment, path]),
mockFileFormData.metadata['dc.title'],
true,
);
path = 'metadata/dc.description';
expect(operationsBuilder.add).toHaveBeenCalledWith(
pathCombiner.getPath(path),
pathCombiner.getPath([...pathFragment, path]),
mockFileFormData.metadata['dc.description'],
true,
);
path = 'accessConditions';
expect(operationsBuilder.add).toHaveBeenCalledWith(
pathCombiner.getPath(path),
pathCombiner.getPath([...pathFragment, path]),
accessConditionsToSave,
true,
);
@@ -308,6 +293,28 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
}));
it('should update Bitstream data properly when access options are omitted', fakeAsync(() => {
compAsAny.formRef = {formGroup: null};
compAsAny.fileData = fileData;
compAsAny.pathCombiner = pathCombiner;
formService.validateAllFormFields.and.callFake(() => null);
formService.isValid.and.returnValue(of(true));
formService.getFormData.and.returnValue(of(noAccessConditionsMock));
const response = [
Object.assign(mockSubmissionObject, {
sections: {
upload: {
files: mockUploadFiles
}
}
})
];
operationsService.jsonPatchByResourceID.and.returnValue(of(response));
comp.saveBitstreamData();
tick();
expect(uploadService.updateFileData).toHaveBeenCalled();
}));
it('should not save Bitstream File data properly when form is not valid', fakeAsync(() => {
compAsAny.formRef = { formGroup: null };
compAsAny.pathCombiner = pathCombiner;

View File

@@ -1,31 +1,11 @@
import {
ChangeDetectorRef,
Component,
OnDestroy,
OnInit,
ViewChild,
} from '@angular/core';
import { ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { UntypedFormControl } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import {
DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER,
DynamicDatePickerModel,
DynamicFormArrayModel,
DynamicFormControlEvent,
DynamicFormControlModel,
DynamicFormGroupModel,
DynamicSelectModel,
MATCH_ENABLED,
OR_OPERATOR,
} from '@ng-dynamic-forms/core';
import { DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER, DynamicDatePickerModel, DynamicFormArrayModel, DynamicFormControlEvent, DynamicFormControlModel, DynamicFormGroupModel, DynamicSelectModel, MATCH_ENABLED, OR_OPERATOR } from '@ng-dynamic-forms/core';
import { DynamicDateControlValue } from '@ng-dynamic-forms/core/lib/model/dynamic-date-control.model';
import { DynamicFormControlCondition } from '@ng-dynamic-forms/core/lib/model/misc/dynamic-form-control-relation.model';
import { Subscription } from 'rxjs';
import {
filter,
mergeMap,
take,
} from 'rxjs/operators';
import { filter, mergeMap, take } from 'rxjs/operators';
import { AccessConditionOption } from '../../../../../core/config/models/config-access-condition-option.model';
import { SubmissionFormsModel } from '../../../../../core/config/models/config-submission-forms.model';
@@ -36,12 +16,7 @@ import { WorkspaceitemSectionUploadObject } from '../../../../../core/submission
import { WorkspaceitemSectionUploadFileObject } from '../../../../../core/submission/models/workspaceitem-section-upload-file.model';
import { SubmissionJsonPatchOperationsService } from '../../../../../core/submission/submission-json-patch-operations.service';
import { dateToISOFormat } from '../../../../../shared/date.util';
import {
hasNoValue,
hasValue,
isNotEmpty,
isNotNull,
} from '../../../../../shared/empty.util';
import { hasNoValue, hasValue, isNotEmpty, isNotNull } from '../../../../../shared/empty.util';
import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service';
import { FormFieldModel } from '../../../../../shared/form/builder/models/form-field.model';
import { FormComponent } from '../../../../../shared/form/form.component';
@@ -49,20 +24,8 @@ import { FormService } from '../../../../../shared/form/form.service';
import { SubmissionService } from '../../../../submission.service';
import { POLICY_DEFAULT_WITH_LIST } from '../../section-upload.component';
import { SectionUploadService } from '../../section-upload.service';
import {
BITSTREAM_ACCESS_CONDITION_GROUP_CONFIG,
BITSTREAM_ACCESS_CONDITION_GROUP_LAYOUT,
BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_CONFIG,
BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_LAYOUT,
BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_CONFIG,
BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_LAYOUT,
BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_CONFIG,
BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_LAYOUT,
BITSTREAM_FORM_ACCESS_CONDITION_TYPE_CONFIG,
BITSTREAM_FORM_ACCESS_CONDITION_TYPE_LAYOUT,
BITSTREAM_METADATA_FORM_GROUP_CONFIG,
BITSTREAM_METADATA_FORM_GROUP_LAYOUT,
} from './section-upload-file-edit.model';
import { BITSTREAM_ACCESS_CONDITION_GROUP_CONFIG, BITSTREAM_ACCESS_CONDITION_GROUP_LAYOUT, BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_CONFIG, BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_LAYOUT, BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_CONFIG, BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_LAYOUT, BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_CONFIG, BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_LAYOUT, BITSTREAM_FORM_ACCESS_CONDITION_TYPE_CONFIG, BITSTREAM_FORM_ACCESS_CONDITION_TYPE_LAYOUT, BITSTREAM_FORM_PRIMARY, BITSTREAM_FORM_PRIMARY_LAYOUT, BITSTREAM_METADATA_FORM_GROUP_CONFIG, BITSTREAM_METADATA_FORM_GROUP_LAYOUT } from './section-upload-file-edit.model';
import { DynamicCustomSwitchModel } from 'src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model';
/**
* This component represents the edit form for bitstream
@@ -80,6 +43,13 @@ implements OnInit, OnDestroy {
*/
@ViewChild('formRef') public formRef: FormComponent;
/**
* The indicator is the primary bitstream
* it will be null if no primary bitstream is set for the ORIGINAL bundle
* @type {boolean, null}
*/
isPrimary: boolean;
/**
* The list of available access condition
* @type {Array}
@@ -197,6 +167,10 @@ implements OnInit, OnDestroy {
* The form model
*/
public initModelData(formModel: DynamicFormControlModel[]) {
const primaryBitstreamModel: any = this.formBuilderService.findById('primary', formModel, this.fileIndex);
primaryBitstreamModel.value = this.isPrimary || false;
this.fileData.accessConditions.forEach((accessCondition, index) => {
Array.of('name', 'startDate', 'endDate')
.filter((key) => accessCondition.hasOwnProperty(key) && isNotEmpty(accessCondition[key]))
@@ -297,6 +271,9 @@ implements OnInit, OnDestroy {
]),
});
const formModel: DynamicFormControlModel[] = [];
formModel.push(new DynamicCustomSwitchModel(BITSTREAM_FORM_PRIMARY, BITSTREAM_FORM_PRIMARY_LAYOUT));
const metadataGroupModelConfig = Object.assign({}, BITSTREAM_METADATA_FORM_GROUP_CONFIG);
metadataGroupModelConfig.group = this.formBuilderService.modelFromConfiguration(
this.submissionId,
@@ -392,10 +369,14 @@ implements OnInit, OnDestroy {
return formModel;
}
/**
* Save bitstream metadata
*/
saveBitstreamData() {
const pathFragment = ['files', this.fileIndex];
// validate form
this.formService.validateAllFormFields(this.formRef.formGroup);
const saveBitstreamDataSubscription = this.formService.isValid(this.formId).pipe(
@@ -404,13 +385,15 @@ implements OnInit, OnDestroy {
mergeMap(() => this.formService.getFormData(this.formId)),
take(1),
mergeMap((formData: any) => {
this.uploadService.updatePrimaryBitstreamOperation(this.pathCombiner.getPath('primary'), this.isPrimary, formData.primary[0], this.fileId);
// collect bitstream metadata
Object.keys((formData.metadata))
.filter((key) => isNotEmpty(formData.metadata[key]))
.forEach((key) => {
const metadataKey = key.replace(/_/g, '.');
const path = `metadata/${metadataKey}`;
this.operationsBuilder.add(this.pathCombiner.getPath(path), formData.metadata[key], true);
this.operationsBuilder.add(this.pathCombiner.getPath([...pathFragment, path]), formData.metadata[key], true);
});
Object.keys((this.fileData.metadata))
.filter((key) => isNotEmpty(this.fileData.metadata[key]))
@@ -419,22 +402,24 @@ implements OnInit, OnDestroy {
.forEach((key) => {
const metadataKey = key.replace(/_/g, '.');
const path = `metadata/${metadataKey}`;
this.operationsBuilder.remove(this.pathCombiner.getPath(path));
this.operationsBuilder.remove(this.pathCombiner.getPath([...pathFragment, path]));
});
const accessConditionsToSave = [];
formData.accessConditions
.map((accessConditions) => accessConditions.accessConditionGroup)
.filter((accessCondition) => isNotEmpty(accessCondition))
.forEach((accessCondition) => {
let accessConditionOpt;
if (formData.hasOwnProperty('accessConditions')) {
formData.accessConditions
.filter((accessConditions) => isNotNull(accessConditions))
.map((accessConditions) => accessConditions.accessConditionGroup)
.filter((accessCondition) => isNotEmpty(accessCondition))
.forEach((accessCondition) => {
let accessConditionOpt;
this.availableAccessConditionOptions
.filter((element) => isNotNull(accessCondition.name) && element.name === accessCondition.name[0].value)
.forEach((element) => accessConditionOpt = element);
this.availableAccessConditionOptions
.filter((element) => isNotNull(accessCondition.name) && element.name === accessCondition.name[0].value)
.forEach((element) => accessConditionOpt = element);
if (accessConditionOpt) {
const currentAccessCondition = Object.assign({}, accessCondition);
currentAccessCondition.name = this.retrieveValueFromField(accessCondition.name);
if (accessConditionOpt) {
const currentAccessCondition = Object.assign({}, accessCondition);
currentAccessCondition.name = this.retrieveValueFromField(accessCondition.name);
/* When start and end date fields are deactivated, their values may be still present in formData,
therefore it is necessary to delete them if they're not allowed by the current access condition option. */
@@ -471,27 +456,31 @@ implements OnInit, OnDestroy {
accessConditionsToSave.push(currentAccessCondition);
}
});
if (isNotEmpty(accessConditionsToSave)) {
this.operationsBuilder.add(this.pathCombiner.getPath('accessConditions'), accessConditionsToSave, true);
}
// dispatch a PATCH request to save metadata
return this.operationsService.jsonPatchByResourceID(
this.submissionService.getSubmissionObjectLinkName(),
this.submissionId,
this.pathCombiner.rootElement,
this.pathCombiner.subRootElement);
if (isNotEmpty(accessConditionsToSave)) {
this.operationsBuilder.add(this.pathCombiner.getPath([...pathFragment, 'accessConditions']), accessConditionsToSave, true);
}
// dispatch a PATCH request to save metadata
return this.operationsService.jsonPatchByResourceID(
this.submissionService.getSubmissionObjectLinkName(),
this.submissionId,
this.pathCombiner.rootElement,
this.pathCombiner.subRootElement);
}),
).subscribe((result: SubmissionObject[]) => {
if (result[0].sections[this.sectionId]) {
const uploadSection = (result[0].sections[this.sectionId] as WorkspaceitemSectionUploadObject);
Object.keys(uploadSection.files)
.filter((key) => uploadSection.files[key].uuid === this.fileId)
.forEach((key) => this.uploadService.updateFileData(
this.submissionId, this.sectionId, this.fileId, uploadSection.files[key]),
);
const section = result[0].sections[this.sectionId];
if (!section) {
return;
}
const uploadSection = (section as WorkspaceitemSectionUploadObject);
this.uploadService.updateFilePrimaryBitstream(this.submissionId, this.sectionId, uploadSection.primary);
Object.keys(uploadSection.files)
.filter((key) => uploadSection.files[key].uuid === this.fileId)
.forEach((key) => this.uploadService.updateFileData(
this.submissionId, this.sectionId, this.fileId, uploadSection.files[key]),
);
this.isSaving = false;
this.activeModal.close();
});

View File

@@ -1,12 +1,4 @@
import {
DynamicDatePickerModelConfig,
DynamicFormArrayModelConfig,
DynamicFormControlLayout,
DynamicFormGroupModelConfig,
DynamicSelectModelConfig,
MATCH_ENABLED,
OR_OPERATOR,
} from '@ng-dynamic-forms/core';
import { DynamicDatePickerModelConfig, DynamicFormArrayModelConfig, DynamicFormControlLayout, DynamicFormGroupModelConfig, DynamicSelectModelConfig, DynamicSwitchModelConfig, MATCH_ENABLED, OR_OPERATOR } from '@ng-dynamic-forms/core';
export const BITSTREAM_METADATA_FORM_GROUP_CONFIG: DynamicFormGroupModelConfig = {
id: 'metadata',
@@ -56,6 +48,19 @@ export const BITSTREAM_FORM_ACCESS_CONDITION_TYPE_LAYOUT: DynamicFormControlLayo
label: 'col-form-label name-label',
},
};
export const BITSTREAM_FORM_PRIMARY_LAYOUT: DynamicFormControlLayout = {
element: {
host: 'col-12',
container: 'text-right'
},
};
export const BITSTREAM_FORM_PRIMARY: DynamicSwitchModelConfig = {
id: 'primary',
name: 'primary',
label: 'bitstream.edit.form.primaryBitstream.label'
};
export const BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_CONFIG: DynamicDatePickerModelConfig = {
id: 'startDate',

View File

@@ -1,6 +1,22 @@
<ng-container *ngIf="fileData">
<div class="row">
<div class="col-md-12">
<!-- Default switch -->
<div class="col-md-2 d-flex justify-content-center align-items-center" >
<div class="custom-control custom-switch">
<input
type="checkbox"
class="custom-control-input"
id="primaryBitstream{{fileIndex}}"
[disabled]="processingSaveStatus$ | async"
[checked]="isPrimary"
(change)="togglePrimaryBitstream($event)">
<label class="custom-control-label" for="primaryBitstream{{fileIndex}}">
<span class="sr-only" *ngIf="!isPrimary">{{'submission.sections.upload.primary.make' | translate:{ fileName: fileName } }}</span>
<span class="sr-only" *ngIf="isPrimary">{{'submission.sections.upload.primary.remove' | translate:{ fileName: fileName } }}</span>
</label>
</div>
</div>
<div class="col-md-10">
<div class="float-left w-75">
<h3>{{fileName}} <span class="text-muted">({{fileData?.sizeBytes | dsFileSize}})</span></h3>
</div>

View File

@@ -1,28 +1,10 @@
import { CommonModule } from '@angular/common';
import {
ChangeDetectorRef,
Component,
NO_ERRORS_SCHEMA,
} from '@angular/core';
import {
ComponentFixture,
inject,
TestBed,
waitForAsync,
} from '@angular/core/testing';
import {
BrowserModule,
By,
} from '@angular/platform-browser';
import {
NgbModal,
NgbModule,
} from '@ng-bootstrap/ng-bootstrap';
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
import { BrowserModule, By } from '@angular/platform-browser';
import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import {
of as observableOf,
of,
} from 'rxjs';
import { of as observableOf, of } from 'rxjs';
import { JsonPatchOperationPathCombiner } from '../../../../core/json-patch/builder/json-patch-operation-path-combiner';
import { JsonPatchOperationsBuilder } from '../../../../core/json-patch/builder/json-patch-operations-builder';
@@ -32,12 +14,7 @@ import { FormBuilderService } from '../../../../shared/form/builder/form-builder
import { FormService } from '../../../../shared/form/form.service';
import { getMockFormService } from '../../../../shared/mocks/form-service.mock';
import { getMockSectionUploadService } from '../../../../shared/mocks/section-upload.service.mock';
import {
mockSubmissionCollectionId,
mockSubmissionId,
mockUploadConfigResponse,
mockUploadFiles,
} from '../../../../shared/mocks/submission.mock';
import { mockSubmissionCollectionId, mockSubmissionId, mockUploadConfigResponse, mockUploadFiles } from '../../../../shared/mocks/submission.mock';
import { HALEndpointServiceStub } from '../../../../shared/testing/hal-endpoint-service.stub';
import { SubmissionJsonPatchOperationsServiceStub } from '../../../../shared/testing/submission-json-patch-operations-service.stub';
import { SubmissionServiceStub } from '../../../../shared/testing/submission-service.stub';
@@ -82,7 +59,7 @@ describe('SubmissionSectionUploadFileComponent test suite', () => {
const fileName = '123456-test-upload.jpg';
const fileId = '123456-test-upload';
const fileData: any = mockUploadFiles[0];
const pathCombiner = new JsonPatchOperationPathCombiner('sections', sectionId, 'files', fileIndex);
const pathCombiner = new JsonPatchOperationPathCombiner('sections', sectionId);
const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', {
add: jasmine.createSpy('add'),
@@ -196,7 +173,7 @@ describe('SubmissionSectionUploadFileComponent test suite', () => {
expect(comp.fileData).toEqual(fileData);
});
it('should call deleteFile on delete confirmation', () => {
it('should call deleteFile on delete confirmation', async () => {
spyOn(compAsAny, 'deleteFile');
comp.fileData = fileData;
@@ -212,9 +189,25 @@ describe('SubmissionSectionUploadFileComponent test suite', () => {
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(compAsAny.deleteFile).toHaveBeenCalled();
});
await fixture.whenStable();
expect(compAsAny.deleteFile).toHaveBeenCalled();
});
it('should delete primary if file we delete is primary', () => {
compAsAny.isPrimary = true;
compAsAny.pathCombiner = pathCombiner;
operationsService.jsonPatchByResourceID.and.returnValue(observableOf({}));
compAsAny.deleteFile();
expect(operationsBuilder.remove).toHaveBeenCalledWith(pathCombiner.getPath('primary'));
expect(uploadService.updateFilePrimaryBitstream).toHaveBeenCalledWith(submissionId, sectionId, null);
});
it('should NOT delete primary if file we delete is NOT primary', () => {
compAsAny.isPrimary = false;
compAsAny.pathCombiner = pathCombiner;
operationsService.jsonPatchByResourceID.and.returnValue(observableOf({}));
compAsAny.deleteFile();
expect(uploadService.updateFilePrimaryBitstream).not.toHaveBeenCalledTimes(1);
});
it('should delete file properly', () => {
@@ -225,7 +218,8 @@ describe('SubmissionSectionUploadFileComponent test suite', () => {
compAsAny.deleteFile();
expect(uploadService.removeUploadedFile).toHaveBeenCalledWith(submissionId, sectionId, fileId);
expect(operationsBuilder.remove).toHaveBeenCalledWith(pathCombiner.getPath());
expect(operationsBuilder.remove).toHaveBeenCalledWith(pathCombiner.getPath(['files', fileIndex]));
expect(operationsService.jsonPatchByResourceID).toHaveBeenCalledWith(
'workspaceitems',
submissionId,

View File

@@ -1,35 +1,21 @@
import {
ChangeDetectorRef,
Component,
Input,
OnChanges,
OnDestroy,
OnInit,
SimpleChanges,
ViewChild,
} from '@angular/core';
import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild } from '@angular/core';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { NgbModalOptions } from '@ng-bootstrap/ng-bootstrap/modal/modal-config';
import { DynamicFormControlModel } from '@ng-dynamic-forms/core';
import {
BehaviorSubject,
Subscription,
} from 'rxjs';
import { filter } from 'rxjs/operators';
import { SubmissionFormsModel } from '../../../../core/config/models/config-submission-forms.model';
import { SubmissionService } from '../../../submission.service';
import { JsonPatchOperationPathCombiner } from '../../../../core/json-patch/builder/json-patch-operation-path-combiner';
import { JsonPatchOperationsBuilder } from '../../../../core/json-patch/builder/json-patch-operations-builder';
import { Bitstream } from '../../../../core/shared/bitstream.model';
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
import { WorkspaceitemSectionUploadFileObject } from '../../../../core/submission/models/workspaceitem-section-upload-file.model';
import { SubmissionJsonPatchOperationsService } from '../../../../core/submission/submission-json-patch-operations.service';
import {
hasValue,
isNotUndefined,
} from '../../../../shared/empty.util';
import { hasValue, isNotUndefined } from '../../../../shared/empty.util';
import { FormService } from '../../../../shared/form/form.service';
import { SubmissionService } from '../../../submission.service';
import { SectionUploadService } from '../section-upload.service';
import { SubmissionSectionUploadFileEditComponent } from './edit/section-upload-file-edit.component';
@@ -42,6 +28,12 @@ import { SubmissionSectionUploadFileEditComponent } from './edit/section-upload-
templateUrl: './section-upload-file.component.html',
})
export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit, OnDestroy {
/**
* The indicator is the primary bitstream
* it will be null if no primary bitstream is set for the ORIGINAL bundle
* @type {boolean, null}
*/
@Input() isPrimary: boolean | null;
/**
* The list of available access condition
@@ -105,6 +97,11 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit,
*/
@ViewChild(SubmissionSectionUploadFileEditComponent) fileEditComp: SubmissionSectionUploadFileEditComponent;
/**
* A boolean representing if a submission save operation is pending
* @type {Observable<boolean>}
*/
public processingSaveStatus$: Observable<boolean>;
/**
* The bitstream's metadata data
@@ -142,6 +139,12 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit,
*/
protected pathCombiner: JsonPatchOperationPathCombiner;
/**
* The [JsonPatchOperationPathCombiner] object
* @type {JsonPatchOperationPathCombiner}
*/
protected primaryBitstreamPathCombiner: JsonPatchOperationPathCombiner;
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
@@ -167,9 +170,7 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit,
* @param {SectionUploadService} uploadService
*/
constructor(
private cdr: ChangeDetectorRef,
private formService: FormService,
private halService: HALEndpointService,
private modalService: NgbModal,
private operationsBuilder: JsonPatchOperationsBuilder,
private operationsService: SubmissionJsonPatchOperationsService,
@@ -202,7 +203,8 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit,
*/
ngOnInit() {
this.formId = this.formService.getUniqueId(this.fileId);
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionId, 'files', this.fileIndex);
this.processingSaveStatus$ = this.submissionService.getSubmissionSaveProcessingStatus(this.submissionId);
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionId);
this.loadFormMetadata();
}
@@ -252,7 +254,12 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit,
activeModal.componentInstance.formMetadata = this.formMetadata;
activeModal.componentInstance.pathCombiner = this.pathCombiner;
activeModal.componentInstance.submissionId = this.submissionId;
activeModal.componentInstance.isPrimary = this.isPrimary;
}
togglePrimaryBitstream(event) {
this.uploadService.updatePrimaryBitstreamOperation(this.pathCombiner.getPath('primary'), this.isPrimary, event.target.checked, this.fileId);
this.submissionService.dispatchSaveSection(this.submissionId, this.sectionId);
}
ngOnDestroy(): void {
@@ -278,13 +285,20 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit,
* Delete bitstream from submission
*/
protected deleteFile() {
this.operationsBuilder.remove(this.pathCombiner.getPath());
this.operationsBuilder.remove(this.pathCombiner.getPath(['files', this.fileIndex]));
if (this.isPrimary) {
this.operationsBuilder.remove(this.pathCombiner.getPath('primary'));
}
this.subscriptions.push(this.operationsService.jsonPatchByResourceID(
this.submissionService.getSubmissionObjectLinkName(),
this.submissionId,
this.pathCombiner.rootElement,
this.pathCombiner.subRootElement)
.subscribe(() => {
if (this.isPrimary) {
this.uploadService.updateFilePrimaryBitstream(this.submissionId, this.sectionId, null);
}
this.uploadService.removeUploadedFile(this.submissionId, this.sectionId, this.fileId);
this.processingDelete$.next(false);
}));

View File

@@ -1,7 +1,4 @@
import {
Component,
Input,
} from '@angular/core';
import { Component, Input } from '@angular/core';
import { SubmissionFormsModel } from 'src/app/core/config/models/config-submission-forms.model';
import { ThemedComponent } from 'src/app/shared/theme-support/themed.component';
@@ -21,6 +18,13 @@ export class ThemedSubmissionSectionUploadFileComponent
*/
@Input() availableAccessConditionOptions: any[];
/**
* The indicator is the primary bitstream
* it will be null if no primary bitstream is set for the ORIGINAL bundle
* @type {boolean, null}
*/
@Input() isPrimary: boolean | null;
/**
* The submission id
* @type {string}
@@ -73,6 +77,7 @@ export class ThemedSubmissionSectionUploadFileComponent
protected inAndOutputNames: (keyof SubmissionSectionUploadFileComponent & keyof this)[] = [
'availableAccessConditionOptions',
'isPrimary',
'collectionId',
'collectionPolicyType',
'configMetadataForm',

View File

@@ -2,15 +2,7 @@
[dismissible]="true"
[type]="AlertTypeEnum.Info"></ds-alert>
<ng-container *ngIf="fileList.length === 0">
<div class="row">
<div class="col-md-12">
<h3 class="text-center"><span class="text-muted">{{'submission.sections.upload.no-file-uploaded' | translate}}</span></h3>
</div>
</div>
</ng-container>
<ng-container *ngIf="fileList.length > 0">
<ng-container *ngIf="fileList.length > 0; else noFileUploaded">
<div *ngIf="collectionDefaultAccessConditions.length > 0" class="row">
<div class="col-sm-12" >
@@ -26,16 +18,26 @@
</ds-alert>
</div>
</div>
<ng-container *ngFor="let fileEntry of fileList">
<div class="row">
<div class="col-md-2">
<span class="text-left font-weight-bold">{{ 'bitstream.edit.form.primaryBitstream.label' | translate }}</span>
</div>
</div>
<div class="row">
<div class="col-md-12">
<hr/>
</div>
</div>
<ng-container *ngFor="let fileEntry of fileList; let i = index;">
<ds-themed-submission-upload-section-file
[isPrimary]="primaryBitstreamUUID ? primaryBitstreamUUID === fileEntry.uuid : null"
[availableAccessConditionOptions]="availableAccessConditionOptions"
[collectionId]="collectionId"
[collectionPolicyType]="collectionPolicyType"
[configMetadataForm]="(configMetadataForm$ | async)"
[fileId]="fileIndexes[fileList.indexOf(fileEntry)]"
[fileIndex]="fileList.indexOf(fileEntry)"
[fileName]="fileNames[fileList.indexOf(fileEntry)]"
[fileId]="fileEntry.uuid"
[fileIndex]="i"
[fileName]="fileNames[i]"
[sectionId]="sectionData.id"
[submissionId]="submissionId"></ds-themed-submission-upload-section-file>
<div class="row">
@@ -45,3 +47,11 @@
</div>
</ng-container>
</ng-container>
<ng-template #noFileUploaded>
<div class="row">
<div class="col-md-12">
<h3 class="text-center"><span class="text-muted">{{'submission.sections.upload.no-file-uploaded' | translate}}</span></h3>
</div>
</div>
</ng-template>

View File

@@ -35,6 +35,7 @@ import {
mockUploadConfigResponse,
mockUploadConfigResponseNotRequired,
mockUploadFiles,
mockUploadFilesData,
} from '../../../shared/mocks/submission.mock';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { SectionsServiceStub } from '../../../shared/testing/sections-service.stub';
@@ -168,6 +169,7 @@ describe('SubmissionSectionUploadComponent test suite', () => {
);
bitstreamService.getUploadedFileList.and.returnValue(observableOf([]));
bitstreamService.getUploadedFilesData.and.returnValue(observableOf({ primary: null, files: [] }));
};
TestBed.configureTestingModule({
@@ -238,7 +240,7 @@ describe('SubmissionSectionUploadComponent test suite', () => {
});
it('should init component properly', () => {
bitstreamService.getUploadedFilesData.and.returnValue(observableOf({ primary: null, files: [] }));
submissionServiceStub.getSubmissionObject.and.returnValue(observableOf(submissionState));
collectionDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(Object.assign(new Collection(), mockCollection, {
@@ -254,15 +256,8 @@ describe('SubmissionSectionUploadComponent test suite', () => {
createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup)),
);
bitstreamService.getUploadedFileList.and.returnValue(observableOf([]));
comp.onSectionInit();
const expectedGroupsMap = new Map([
[mockUploadConfigResponse.accessConditionOptions[1].name, [mockGroup as any]],
[mockUploadConfigResponse.accessConditionOptions[2].name, [mockGroup as any]],
]);
expect(comp.collectionId).toBe(collectionId);
expect(comp.collectionName).toBe(mockCollection.name);
expect(comp.availableAccessConditionOptions.length).toBe(4);
@@ -270,12 +265,12 @@ describe('SubmissionSectionUploadComponent test suite', () => {
expect(comp.required$.getValue()).toBe(true);
expect(compAsAny.subs.length).toBe(2);
expect(compAsAny.fileList).toEqual([]);
expect(compAsAny.fileIndexes).toEqual([]);
expect(compAsAny.fileNames).toEqual([]);
expect(compAsAny.primaryBitstreamUUID).toEqual(null);
});
it('should init file list properly', () => {
bitstreamService.getUploadedFilesData.and.returnValue(observableOf({ primary: null, files: [] }));
submissionServiceStub.getSubmissionObject.and.returnValue(observableOf(submissionState));
@@ -290,7 +285,7 @@ describe('SubmissionSectionUploadComponent test suite', () => {
createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup)),
);
bitstreamService.getUploadedFileList.and.returnValue(observableOf(mockUploadFiles));
bitstreamService.getUploadedFilesData.and.returnValue(observableOf(mockUploadFilesData));
comp.onSectionInit();
@@ -306,12 +301,14 @@ describe('SubmissionSectionUploadComponent test suite', () => {
expect(comp.required$.getValue()).toBe(true);
expect(compAsAny.subs.length).toBe(2);
expect(compAsAny.fileList).toEqual(mockUploadFiles);
expect(compAsAny.fileIndexes).toEqual(['123456-test-upload']);
expect(compAsAny.primaryBitstreamUUID).toEqual(null);
expect(compAsAny.fileNames).toEqual(['123456-test-upload.jpg']);
});
it('should properly read the section status when required is true', () => {
bitstreamService.getUploadedFilesData.and.returnValue(observableOf({ primary: null, files: [] }));
submissionServiceStub.getSubmissionObject.and.returnValue(observableOf(submissionState));
collectionDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockCollection));
@@ -332,7 +329,7 @@ describe('SubmissionSectionUploadComponent test suite', () => {
comp.onSectionInit();
expect(comp.required$.getValue()).toBe(true);
expect(comp.required$.getValue()).toBe(true);
expect(compAsAny.getSectionStatus()).toBeObservable(cold('-c-d', {
c: false,
@@ -343,6 +340,7 @@ describe('SubmissionSectionUploadComponent test suite', () => {
it('should properly read the section status when required is false', () => {
submissionServiceStub.getSubmissionObject.and.returnValue(observableOf(submissionState));
bitstreamService.getUploadedFilesData.and.returnValue(observableOf({ primary: null, files: [] }));
collectionDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockCollection));
resourcePolicyService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$(mockDefaultAccessCondition));

View File

@@ -1,22 +1,6 @@
import {
ChangeDetectorRef,
Component,
Inject,
} from '@angular/core';
import {
BehaviorSubject,
combineLatest as observableCombineLatest,
Observable,
Subscription,
} from 'rxjs';
import {
distinctUntilChanged,
filter,
map,
mergeMap,
switchMap,
tap,
} from 'rxjs/operators';
import { ChangeDetectorRef, Component, Inject } from '@angular/core';
import { BehaviorSubject, combineLatest as observableCombineLatest, combineLatest, Observable, Subscription } from 'rxjs';
import { distinctUntilChanged, filter, map, mergeMap, switchMap, tap } from 'rxjs/operators';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { AccessConditionOption } from '../../../core/config/models/config-access-condition-option.model';
@@ -31,18 +15,14 @@ import { ResourcePolicyDataService } from '../../../core/resource-policy/resourc
import { Collection } from '../../../core/shared/collection.model';
import { getFirstSucceededRemoteData } from '../../../core/shared/operators';
import { AlertType } from '../../../shared/alert/alert-type';
import {
hasValue,
isNotEmpty,
isNotUndefined,
isUndefined,
} from '../../../shared/empty.util';
import { hasValue, isNotEmpty, isNotUndefined, isUndefined } from '../../../shared/empty.util';
import { followLink } from '../../../shared/utils/follow-link-config.model';
import { SubmissionObjectEntry } from '../../objects/submission-objects.reducer';
import { SubmissionService } from '../../submission.service';
import { SectionModelComponent } from '../models/section.model';
import { SectionDataObject } from '../models/section-data.model';
import { SectionsService } from '../sections.service';
import { WorkspaceitemSectionUploadObject } from 'src/app/core/submission/models/workspaceitem-section-upload.model';
import { renderSectionFor } from '../sections-decorator';
import { SectionsType } from '../sections-type';
import { SectionUploadService } from './section-upload.service';
@@ -73,10 +53,10 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent {
public AlertTypeEnum = AlertType;
/**
* The array containing the keys of file list array
* The uuid of primary bitstream file
* @type {Array}
*/
public fileIndexes: string[] = [];
public primaryBitstreamUUID: string | null = null;
/**
* The file list
@@ -209,29 +189,20 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent {
this.changeDetectorRef.detectChanges();
}),
// retrieve submission's bitstreams from state
observableCombineLatest(this.configMetadataForm$,
this.bitstreamService.getUploadedFileList(this.submissionId, this.sectionData.id)).pipe(
filter(([configMetadataForm, fileList]: [SubmissionFormsModel, any[]]) => {
return isNotEmpty(configMetadataForm) && isNotUndefined(fileList);
// retrieve submission's bitstream data from state
combineLatest([this.configMetadataForm$,
this.bitstreamService.getUploadedFilesData(this.submissionId, this.sectionData.id)]).pipe(
filter(([configMetadataForm, { files }]: [SubmissionFormsModel, WorkspaceitemSectionUploadObject]) => {
return isNotEmpty(configMetadataForm) && isNotEmpty(files);
}),
distinctUntilChanged())
.subscribe(([configMetadataForm, fileList]: [SubmissionFormsModel, any[]]) => {
this.fileList = [];
this.fileIndexes = [];
this.fileNames = [];
this.changeDetectorRef.detectChanges();
if (isNotUndefined(fileList) && fileList.length > 0) {
fileList.forEach((file) => {
this.fileList.push(file);
this.fileIndexes.push(file.uuid);
this.fileNames.push(this.getFileName(configMetadataForm, file));
});
.subscribe(([configMetadataForm, { primary, files }]: [SubmissionFormsModel, WorkspaceitemSectionUploadObject]) => {
this.primaryBitstreamUUID = primary;
this.fileList = files;
this.fileNames = Array.from(files, file => this.getFileName(configMetadataForm, file));
}
this.changeDetectorRef.detectChanges();
},
),
)
);
}

View File

@@ -0,0 +1,63 @@
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TestBed, waitForAsync } from '@angular/core/testing';
import { JsonPatchOperationPathCombiner } from 'src/app/core/json-patch/builder/json-patch-operation-path-combiner';
import { JsonPatchOperationsBuilder } from 'src/app/core/json-patch/builder/json-patch-operations-builder';
import { SectionUploadService } from './section-upload.service';
import { Store, StoreModule } from '@ngrx/store';
const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', {
add: jasmine.createSpy('add'),
replace: jasmine.createSpy('replace'),
remove: jasmine.createSpy('remove'),
});
describe('SectionUploadService test suite', () => {
let sectionUploadService: SectionUploadService;
let operationsBuilder: any;
const pathCombiner = new JsonPatchOperationPathCombiner('sections', 'upload');
const primaryPath = pathCombiner.getPath('primary');
const fileId = 'test';
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [StoreModule],
providers: [
{ provide: Store, useValue: {} },
SectionUploadService,
{ provide: JsonPatchOperationsBuilder, useValue: jsonPatchOpBuilder },
],
schemas: [NO_ERRORS_SCHEMA]
});
}));
beforeEach(() => {
sectionUploadService = TestBed.inject(SectionUploadService);
operationsBuilder = TestBed.inject(JsonPatchOperationsBuilder);
});
[
{
initialPrimary: null,
primary: true,
operationName: 'add',
expected: [primaryPath, fileId, false, true]
},
{
initialPrimary: true,
primary: false,
operationName: 'remove',
expected: [primaryPath]
},
{
initialPrimary: false,
primary: true,
operationName: 'replace',
expected: [primaryPath, fileId, true]
}
].forEach(({ initialPrimary, primary, operationName, expected }) => {
it(`updatePrimaryBitstreamOperation should add ${operationName} operation`, () => {
const path = pathCombiner.getPath('primary');
sectionUploadService.updatePrimaryBitstreamOperation(path, initialPrimary, primary, fileId);
expect(operationsBuilder[operationName]).toHaveBeenCalledWith(...expected);
});
});
});

View File

@@ -1,24 +1,21 @@
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import {
distinctUntilChanged,
filter,
map,
} from 'rxjs/operators';
import { distinctUntilChanged, filter, map } from 'rxjs/operators';
import { WorkspaceitemSectionUploadFileObject } from '../../../core/submission/models/workspaceitem-section-upload-file.model';
import { isUndefined } from '../../../shared/empty.util';
import { submissionSectionDataFromIdSelector, submissionUploadedFileFromUuidSelector, submissionUploadedFilesFromIdSelector } from '../../selectors';
import { WorkspaceitemSectionUploadObject } from 'src/app/core/submission/models/workspaceitem-section-upload.model';
import { JsonPatchOperationPathObject } from 'src/app/core/json-patch/builder/json-patch-operation-path-combiner';
import { JsonPatchOperationsBuilder } from 'src/app/core/json-patch/builder/json-patch-operations-builder';
import { SubmissionState } from '../../submission.reducers';
import {
DeleteUploadedFileAction,
EditFileDataAction,
EditFilePrimaryBitstreamAction,
NewUploadedFileAction,
} from '../../objects/submission-objects.actions';
import {
submissionUploadedFileFromUuidSelector,
submissionUploadedFilesFromIdSelector,
} from '../../selectors';
import { SubmissionState } from '../../submission.reducers';
/**
* A service that provides methods to handle submission's bitstream state.
@@ -30,8 +27,53 @@ export class SectionUploadService {
* Initialize service variables
*
* @param {Store<SubmissionState>} store
* @param {JsonPatchOperationsBuilder} operationsBuilder
*/
constructor(private store: Store<SubmissionState>) {}
constructor(private store: Store<SubmissionState>, private operationsBuilder: JsonPatchOperationsBuilder) {}
/**
* Define and add an operation based on a change
*
* @param path
* The path to endpoint
* @param intitialPrimary
* The initial primary indicator
* @param primary
* the new primary indicator
* @param fileId
* The file id
* @returns {void}
*/
public updatePrimaryBitstreamOperation(path: JsonPatchOperationPathObject, intitialPrimary: boolean | null, primary: boolean | null, fileId: string): void {
if (intitialPrimary === null && primary) {
this.operationsBuilder.add(path, fileId, false, true);
return;
}
if (intitialPrimary !== primary) {
if (primary) {
this.operationsBuilder.replace(path, fileId, true);
return;
}
this.operationsBuilder.remove(path);
}
}
/**
* Return submission's bitstream data from state
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @returns {WorkspaceitemSectionUploadObject}
* Returns submission's bitstream data
*/
public getUploadedFilesData(submissionId: string, sectionId: string): Observable<WorkspaceitemSectionUploadObject> {
return this.store.select(submissionSectionDataFromIdSelector(submissionId, sectionId)).pipe(
map((state) => state),
distinctUntilChanged());
}
/**
* Return submission's bitstream list from state
@@ -110,6 +152,22 @@ export class SectionUploadService {
);
}
/**
* Update primary bitstream into the state
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @param fileUUID
* The bitstream UUID
*/
public updateFilePrimaryBitstream(submissionId: string, sectionId: string, fileUUID: string | null) {
this.store.dispatch(
new EditFilePrimaryBitstreamAction(submissionId, sectionId, fileUUID)
);
}
/**
* Update bitstream metadata into the state
*