mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-17 23:13:04 +00:00
Merge remote-tracking branch 'origin/main' into more-eslint
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
<!--
|
||||
Template for the detect duplicates submission section component
|
||||
@author Kim Shepherd
|
||||
-->
|
||||
<div class="text-sm-left" *ngVar="(this.getDuplicateData() | async) as data">
|
||||
<ng-container *ngIf="data?.potentialDuplicates.length == 0">
|
||||
<div class="alert alert-success w-100">{{ 'submission.sections.duplicates.none' | translate }}</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="data?.potentialDuplicates.length > 0">
|
||||
<div class="alert alert-warning w-100">{{ 'submission.sections.duplicates.detected' | translate }}</div>
|
||||
<div *ngFor="let dupe of data?.potentialDuplicates" class="ds-duplicate">
|
||||
<a target="_blank" [href]="getItemLink(dupe.uuid)">{{dupe.title}}</a>
|
||||
<div *ngFor="let metadatum of Metadata.toViewModelList(dupe.metadata)">
|
||||
{{('item.preview.' + metadatum.key) | translate}} {{metadatum.value}}
|
||||
</div>
|
||||
<p *ngIf="dupe.workspaceItemId">{{ 'submission.sections.duplicates.in-workspace' | translate }}</p>
|
||||
<p *ngIf="dupe.workflowItemId">{{ 'submission.sections.duplicates.in-workflow' | translate }}</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
@@ -0,0 +1,248 @@
|
||||
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
import { NgxPaginationModule } from 'ngx-pagination';
|
||||
import { cold } from 'jasmine-marbles';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||
import { SubmissionService } from '../../submission.service';
|
||||
import { SubmissionServiceStub } from '../../../shared/testing/submission-service.stub';
|
||||
import { SectionsService } from '../sections.service';
|
||||
import { SectionsServiceStub } from '../../../shared/testing/sections-service.stub';
|
||||
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
|
||||
import { getMockFormOperationsService } from '../../../shared/mocks/form-operations-service.mock';
|
||||
import { getMockFormService } from '../../../shared/mocks/form-service.mock';
|
||||
import { FormService } from '../../../shared/form/form.service';
|
||||
import { SubmissionFormsConfigDataService } from '../../../core/config/submission-forms-config-data.service';
|
||||
import { SectionsType } from '../sections-type';
|
||||
import { mockSubmissionCollectionId, mockSubmissionId } from '../../../shared/mocks/submission.mock';
|
||||
import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner';
|
||||
import { SubmissionSectionDuplicatesComponent } from './section-duplicates.component';
|
||||
import { CollectionDataService } from '../../../core/data/collection-data.service';
|
||||
import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder';
|
||||
import { SectionFormOperationsService } from '../form/section-form-operations.service';
|
||||
import { SubmissionScopeType } from '../../../core/submission/submission-scope-type';
|
||||
import { License } from '../../../core/shared/license.model';
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
import { ObjNgFor } from '../../../shared/utils/object-ngfor.pipe';
|
||||
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||
import { Duplicate } from '../../../shared/object-list/duplicate-data/duplicate.model';
|
||||
import { MetadataValue } from '../../../core/shared/metadata.models';
|
||||
import { defaultUUID } from '../../../shared/mocks/uuid.service.mock';
|
||||
import { DUPLICATE } from '../../../shared/object-list/duplicate-data/duplicate.resource-type';
|
||||
|
||||
function getMockSubmissionFormsConfigService(): SubmissionFormsConfigDataService {
|
||||
return jasmine.createSpyObj('FormOperationsService', {
|
||||
getConfigAll: jasmine.createSpy('getConfigAll'),
|
||||
getConfigByHref: jasmine.createSpy('getConfigByHref'),
|
||||
getConfigByName: jasmine.createSpy('getConfigByName'),
|
||||
getConfigBySearch: jasmine.createSpy('getConfigBySearch')
|
||||
});
|
||||
}
|
||||
|
||||
function getMockCollectionDataService(): CollectionDataService {
|
||||
return jasmine.createSpyObj('CollectionDataService', {
|
||||
findById: jasmine.createSpy('findById'),
|
||||
findByHref: jasmine.createSpy('findByHref')
|
||||
});
|
||||
}
|
||||
|
||||
const duplicates: Duplicate[] = [{
|
||||
title: 'Unique title',
|
||||
uuid: defaultUUID,
|
||||
workflowItemId: 1,
|
||||
workspaceItemId: 2,
|
||||
owningCollection: 'Test Collection',
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
Object.assign(new MetadataValue(), {
|
||||
'value': 'Unique title',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 0
|
||||
})]
|
||||
},
|
||||
type: DUPLICATE,
|
||||
_links: {
|
||||
self: {
|
||||
href: 'http://localhost:8080/server/api/core/submission/duplicates/search?uuid=testid'
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
const sectionObject = {
|
||||
header: 'submission.sections.submit.progressbar.duplicates',
|
||||
mandatory: true,
|
||||
opened: true,
|
||||
data: {potentialDuplicates: duplicates},
|
||||
errorsToShow: [],
|
||||
serverValidationErrors: [],
|
||||
id: 'duplicates',
|
||||
sectionType: SectionsType.Duplicates,
|
||||
sectionVisibility: null
|
||||
};
|
||||
|
||||
describe('SubmissionSectionDuplicatesComponent test suite', () => {
|
||||
let comp: SubmissionSectionDuplicatesComponent;
|
||||
let compAsAny: any;
|
||||
let fixture: ComponentFixture<SubmissionSectionDuplicatesComponent>;
|
||||
let submissionServiceStub: any = new SubmissionServiceStub();
|
||||
const sectionsServiceStub: any = new SectionsServiceStub();
|
||||
let formService: any;
|
||||
let formOperationsService: any;
|
||||
let formBuilderService: any;
|
||||
let collectionDataService: any;
|
||||
|
||||
const submissionId = mockSubmissionId;
|
||||
const collectionId = mockSubmissionCollectionId;
|
||||
const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', {
|
||||
add: jasmine.createSpy('add'),
|
||||
replace: jasmine.createSpy('replace'),
|
||||
remove: jasmine.createSpy('remove'),
|
||||
});
|
||||
|
||||
const licenseText = 'License text';
|
||||
const mockCollection = Object.assign(new Collection(), {
|
||||
name: 'Community 1-Collection 1',
|
||||
id: collectionId,
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Community 1-Collection 1'
|
||||
}],
|
||||
license: createSuccessfulRemoteDataObject$(Object.assign(new License(), { text: licenseText }))
|
||||
});
|
||||
const paginationService = new PaginationServiceStub();
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgxPaginationModule,
|
||||
NoopAnimationsModule,
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
declarations: [
|
||||
SubmissionSectionDuplicatesComponent,
|
||||
TestComponent,
|
||||
ObjNgFor,
|
||||
VarDirective,
|
||||
],
|
||||
providers: [
|
||||
{ provide: CollectionDataService, useValue: getMockCollectionDataService() },
|
||||
{ provide: SectionFormOperationsService, useValue: getMockFormOperationsService() },
|
||||
{ provide: FormService, useValue: getMockFormService() },
|
||||
{ provide: JsonPatchOperationsBuilder, useValue: jsonPatchOpBuilder },
|
||||
{ provide: SubmissionFormsConfigDataService, useValue: getMockSubmissionFormsConfigService() },
|
||||
{ provide: NotificationsService, useClass: NotificationsServiceStub },
|
||||
{ provide: SectionsService, useClass: SectionsServiceStub },
|
||||
{ provide: SubmissionService, useClass: SubmissionServiceStub },
|
||||
{ provide: 'collectionIdProvider', useValue: collectionId },
|
||||
{ provide: 'sectionDataProvider', useValue: sectionObject },
|
||||
{ provide: 'submissionIdProvider', useValue: submissionId },
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
ChangeDetectorRef,
|
||||
FormBuilderService
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents().then();
|
||||
}));
|
||||
|
||||
// First test to check the correct component creation
|
||||
describe('', () => {
|
||||
let testComp: TestComponent;
|
||||
let testFixture: ComponentFixture<TestComponent>;
|
||||
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
sectionsServiceStub.isSectionReadOnly.and.returnValue(observableOf(false));
|
||||
sectionsServiceStub.getSectionErrors.and.returnValue(observableOf([]));
|
||||
sectionsServiceStub.getSectionData.and.returnValue(observableOf(sectionObject));
|
||||
testFixture = TestBed.createComponent(SubmissionSectionDuplicatesComponent);
|
||||
testComp = testFixture.componentInstance;
|
||||
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
testFixture.destroy();
|
||||
});
|
||||
|
||||
it('should create SubmissionSectionDuplicatesComponent', () => {
|
||||
expect(testComp).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SubmissionSectionDuplicatesComponent);
|
||||
comp = fixture.componentInstance;
|
||||
compAsAny = comp;
|
||||
submissionServiceStub = TestBed.inject(SubmissionService);
|
||||
formService = TestBed.inject(FormService);
|
||||
formBuilderService = TestBed.inject(FormBuilderService);
|
||||
formOperationsService = TestBed.inject(SectionFormOperationsService);
|
||||
collectionDataService = TestBed.inject(CollectionDataService);
|
||||
compAsAny.pathCombiner = new JsonPatchOperationPathCombiner('sections', sectionObject.id);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.destroy();
|
||||
comp = null;
|
||||
compAsAny = null;
|
||||
});
|
||||
|
||||
// Test initialisation of the submission section
|
||||
it('Should init section properly', () => {
|
||||
collectionDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockCollection));
|
||||
sectionsServiceStub.getSectionErrors.and.returnValue(observableOf([]));
|
||||
sectionsServiceStub.isSectionReadOnly.and.returnValue(observableOf(false));
|
||||
compAsAny.submissionService.getSubmissionScope.and.returnValue(SubmissionScopeType.WorkspaceItem);
|
||||
spyOn(comp, 'getSectionStatus').and.returnValue(observableOf(true));
|
||||
spyOn(comp, 'getDuplicateData').and.returnValue(observableOf({potentialDuplicates: duplicates}));
|
||||
expect(comp.isLoading).toBeTruthy();
|
||||
comp.onSectionInit();
|
||||
fixture.detectChanges();
|
||||
expect(comp.isLoading).toBeFalsy();
|
||||
});
|
||||
|
||||
// The following tests look for proper logic in the getSectionStatus() implementation
|
||||
// These are very simple as we don't really have a 'false' state unless we're still loading
|
||||
it('Should return TRUE if the isLoading is FALSE', () => {
|
||||
compAsAny.isLoading = false;
|
||||
expect(compAsAny.getSectionStatus()).toBeObservable(cold('(a|)', {
|
||||
a: true
|
||||
}));
|
||||
});
|
||||
it('Should return FALSE', () => {
|
||||
compAsAny.isLoadin = true;
|
||||
expect(compAsAny.getSectionStatus()).toBeObservable(cold('(a|)', {
|
||||
a: false
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// declare a test component
|
||||
@Component({
|
||||
selector: 'ds-test-cmp',
|
||||
template: ``
|
||||
})
|
||||
class TestComponent {
|
||||
|
||||
}
|
@@ -0,0 +1,124 @@
|
||||
import {ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
||||
|
||||
import { Observable, of as observableOf, Subscription } from 'rxjs';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { SectionsType } from '../sections-type';
|
||||
import { SectionModelComponent } from '../models/section.model';
|
||||
import { renderSectionFor } from '../sections-decorator';
|
||||
import { SectionDataObject } from '../models/section-data.model';
|
||||
import { SubmissionService } from '../../submission.service';
|
||||
import { AlertType } from '../../../shared/alert/alert-type';
|
||||
import { SectionsService } from '../sections.service';
|
||||
import {
|
||||
WorkspaceitemSectionDuplicatesObject
|
||||
} from '../../../core/submission/models/workspaceitem-section-duplicates.model';
|
||||
import { Metadata } from '../../../core/shared/metadata.utils';
|
||||
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
|
||||
import { getItemModuleRoute } from '../../../item-page/item-page-routing-paths';
|
||||
|
||||
/**
|
||||
* Detect duplicates step
|
||||
*
|
||||
* @author Kim Shepherd
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-submission-section-duplicates',
|
||||
templateUrl: './section-duplicates.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Default
|
||||
})
|
||||
|
||||
@renderSectionFor(SectionsType.Duplicates)
|
||||
export class SubmissionSectionDuplicatesComponent extends SectionModelComponent {
|
||||
protected readonly Metadata = Metadata;
|
||||
/**
|
||||
* The Alert categories.
|
||||
* @type {AlertType}
|
||||
*/
|
||||
public AlertTypeEnum = AlertType;
|
||||
|
||||
/**
|
||||
* Variable to track if the section is loading.
|
||||
* @type {boolean}
|
||||
*/
|
||||
public isLoading = true;
|
||||
|
||||
/**
|
||||
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||
* @type {Array}
|
||||
*/
|
||||
protected subs: Subscription[] = [];
|
||||
|
||||
/**
|
||||
* Initialize instance variables.
|
||||
*
|
||||
* @param {TranslateService} translate
|
||||
* @param {SectionsService} sectionService
|
||||
* @param {SubmissionService} submissionService
|
||||
* @param {string} injectedCollectionId
|
||||
* @param {SectionDataObject} injectedSectionData
|
||||
* @param {string} injectedSubmissionId
|
||||
*/
|
||||
constructor(protected translate: TranslateService,
|
||||
protected sectionService: SectionsService,
|
||||
protected submissionService: SubmissionService,
|
||||
@Inject('collectionIdProvider') public injectedCollectionId: string,
|
||||
@Inject('sectionDataProvider') public injectedSectionData: SectionDataObject,
|
||||
@Inject('submissionIdProvider') public injectedSubmissionId: string) {
|
||||
super(injectedCollectionId, injectedSectionData, injectedSubmissionId);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all instance variables and retrieve configuration.
|
||||
*/
|
||||
onSectionInit() {
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if identifier section has read-only visibility
|
||||
*/
|
||||
isReadOnly(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from all subscriptions, if needed.
|
||||
*/
|
||||
onSectionDestroy(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get section status. Because this simple component never requires human interaction, this is basically
|
||||
* always going to be the opposite of "is this section still loading". This is not the place for API response
|
||||
* error checking but determining whether the step can 'proceed'.
|
||||
*
|
||||
* @return Observable<boolean>
|
||||
* the section status
|
||||
*/
|
||||
public getSectionStatus(): Observable<boolean> {
|
||||
return observableOf(!this.isLoading);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get duplicate data as observable from the section data
|
||||
*/
|
||||
public getDuplicateData(): Observable<WorkspaceitemSectionDuplicatesObject> {
|
||||
return this.sectionService.getSectionData(this.submissionId, this.sectionData.id, this.sectionData.sectionType) as
|
||||
Observable<WorkspaceitemSectionDuplicatesObject>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct and return an item link for use with a preview item stub
|
||||
* @param uuid
|
||||
*/
|
||||
public getItemLink(uuid: any) {
|
||||
return new URLCombiner(getItemModuleRoute(), uuid).toString();
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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);
|
||||
});
|
||||
|
||||
});
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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');
|
||||
|
@@ -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>
|
@@ -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';
|
||||
|
||||
|
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -9,4 +9,6 @@ export enum SectionsType {
|
||||
SherpaPolicies = 'sherpaPolicy',
|
||||
Identifiers = 'identifiers',
|
||||
Collection = 'collection',
|
||||
CoarNotify = 'coarnotify',
|
||||
Duplicates = 'duplicates'
|
||||
}
|
||||
|
@@ -6,7 +6,6 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<ds-form *ngIf="formModel"
|
||||
#formRef="formComponent"
|
||||
[formId]="formId"
|
||||
|
@@ -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;
|
||||
|
@@ -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();
|
||||
});
|
||||
|
@@ -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',
|
||||
|
@@ -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>
|
||||
|
@@ -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,
|
||||
|
@@ -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);
|
||||
}));
|
||||
|
@@ -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',
|
||||
|
@@ -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>
|
||||
|
@@ -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));
|
||||
|
@@ -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();
|
||||
},
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@@ -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
|
||||
*
|
||||
|
Reference in New Issue
Block a user