[CST-4878] Finished working on embargo add part of form and unit testing

This commit is contained in:
Rezart Vata
2021-12-21 13:35:29 +01:00
parent 31fd89a9fc
commit f04f4b4f34
30 changed files with 1219 additions and 101 deletions

View File

@@ -0,0 +1,48 @@
import { autoserialize } from 'cerialize';
/**
* Model class for an Access Condition
*/
export class AccessesConditionOption {
/**
* The name for this Access Condition
*/
name: string;
/**
* The groupName for this Access Condition
*/
groupName: string;
/**
* A boolean representing if this Access Condition has a start date
*/
hasStartDate: boolean;
/**
* A boolean representing if this Access Condition has an end date
*/
hasEndDate: boolean;
/**
* Maximum value of the start date
*/
endDateLimit?: string;
/**
* Maximum value of the end date
*/
startDateLimit?: string;
/**
* Maximum value of the start date
*/
maxStartDate?: string;
/**
* Maximum value of the end date
*/
maxEndDate?: string;
}

View File

@@ -0,0 +1,27 @@
import { autoserialize, inheritSerialization, deserialize } from 'cerialize';
import { typedObject, link } from '../../cache/builders/build-decorators';
import { ConfigObject } from './config.model';
import { AccessesConditionOption } from './config-accesses-conditions-options.model';
import { SUBMISSION_ACCESSES_TYPE } from './config-type';
import { HALLink } from '../../shared/hal-link.model';
@typedObject
@inheritSerialization(ConfigObject)
export class SubmissionAccessModel extends ConfigObject {
static type = SUBMISSION_ACCESSES_TYPE;
/**
* A list of available bitstream access conditions
*/
@autoserialize
accessConditionOptions: AccessesConditionOption[];
@autoserialize
discoverable: boolean;
@deserialize
_links: {
self: HALLink
};
}

View File

@@ -0,0 +1,10 @@
import { inheritSerialization } from 'cerialize';
import { typedObject } from '../../cache/builders/build-decorators';
import { SUBMISSION_ACCESSES_TYPE } from './config-type';
import { SubmissionAccessModel } from './config-submission-access.model';
@typedObject
@inheritSerialization(SubmissionAccessModel)
export class SubmissionAccessesModel extends SubmissionAccessModel {
static type = SUBMISSION_ACCESSES_TYPE;
}

View File

@@ -15,3 +15,5 @@ export const SUBMISSION_SECTION_TYPE = new ResourceType('submissionsection');
export const SUBMISSION_UPLOADS_TYPE = new ResourceType('submissionuploads');
export const SUBMISSION_UPLOAD_TYPE = new ResourceType('submissionupload');
export const SUBMISSION_ACCESSES_TYPE = new ResourceType('submissionaccessoption');

View File

@@ -0,0 +1,44 @@
import { Injectable } from '@angular/core';
import { ConfigService } from './config.service';
import { RequestService } from '../data/request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { dataService } from '../cache/builders/build-decorators';
import { SUBMISSION_ACCESSES_TYPE } from './models/config-type';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http';
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
import { ConfigObject } from './models/config.model';
import { SubmissionAccessesModel } from './models/config-submission-accesses.model';
import { RemoteData } from '../data/remote-data';
import { Observable } from 'rxjs';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { map, switchMap } from 'rxjs/operators';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
/**
* Provides methods to retrieve, from REST server, bitstream access conditions configurations applicable during the submission process.
*/
@Injectable()
@dataService(SUBMISSION_ACCESSES_TYPE)
export class SubmissionAccessesConfigService extends ConfigService {
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<SubmissionAccessesModel>
) {
super(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator, 'submissionaccessoptions');
}
findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow): Observable<RemoteData<SubmissionAccessesModel>> {
return super.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow as FollowLinkConfig<ConfigObject>[]) as Observable<RemoteData<SubmissionAccessesModel>>;
}
}

View File

@@ -163,6 +163,7 @@ import { RootDataService } from './data/root-data.service';
import { Root } from './data/root.model';
import { SearchConfig } from './shared/search/search-filters/search-config.model';
import { SequenceService } from './shared/sequence.service';
import { SubmissionAccessesModel } from 'src/app/core/config/models/config-submission-accesses.model';
/**
* When not in production, endpoint responses can be mocked for testing purposes
@@ -343,7 +344,8 @@ export const models =
Registration,
UsageReport,
Root,
SearchConfig
SearchConfig,
SubmissionAccessesModel
];
@NgModule({

View File

@@ -0,0 +1,37 @@
import { autoserialize, inheritSerialization } from 'cerialize';
import { typedObject } from '../../cache/builders/build-decorators';
import { excludeFromEquals } from '../../utilities/equals.decorators';
import { ResourceType } from '../../shared/resource-type';
import { HALResource } from '../../shared/hal-resource.model';
import { SUBMISSION_ACCESSES } from 'src/app/core/submission/models/submission-accesses.resource-type';
@typedObject
@inheritSerialization(HALResource)
export class SubmissionAccesses extends HALResource {
static type = SUBMISSION_ACCESSES;
/**
* The object type
*/
@excludeFromEquals
@autoserialize
type: ResourceType;
@autoserialize
discoverable: boolean;
@autoserialize
accessConditions: AccessConditions[];
}
export interface AccessConditions {
name: string;
startDate?: Date;
hasStartDate?: boolean;
maxStartDate?: string;
hasEndDate?: boolean;
maxEndDate?: string;
endDate?: Date;
}

View File

@@ -0,0 +1,9 @@
import { ResourceType } from '../../shared/resource-type';
/**
* The resource type for License
*
* Needs to be in a separate file to prevent circular
* dependencies in webpack.
*/
export const SUBMISSION_ACCESSES = new ResourceType('submissionaccesses');

View File

@@ -0,0 +1,18 @@
/**
* An interface to represent the submission's creative commons license section data.
*/
export interface WorkspaceitemSectionAccessesObject {
id: string;
discoverable: boolean;
accessConditions: [
{
name: string;
startDate?: Date;
hasStartDate?: boolean;
maxStartDate?: string;
hasEndDate?: boolean;
maxEndDate?: string;
endDate?: Date;
}
];
}

View File

@@ -1,3 +1,4 @@
import { WorkspaceitemSectionAccessesObject } from './workspaceitem-section-accesses.model';
import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.model';
import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model';
import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model';
@@ -19,4 +20,5 @@ export type WorkspaceitemSectionDataType
| WorkspaceitemSectionFormObject
| WorkspaceitemSectionLicenseObject
| WorkspaceitemSectionCcLicenseObject
| WorkspaceitemSectionAccessesObject
| string;

View File

@@ -0,0 +1,44 @@
import { SubmissionFormsConfigService } from '../../core/config/submission-forms-config.service';
import { SubmissionFormsModel } from 'src/app/core/config/models/config-submission-forms.model';
import { createSuccessfulRemoteDataObject$ } from 'src/app/shared/remote-data.utils';
const configRes = Object.assign(new SubmissionFormsModel(), {
'id': 'AccessConditionDefaultConfiguration',
'canChangeDiscoverable': true,
'accessConditionOptions': [
{
'name': 'openaccess',
'hasStartDate': false,
'hasEndDate': false
},
{
'name': 'lease',
'hasStartDate': false,
'hasEndDate': true,
'maxEndDate': '2022-06-20T12:17:44.420+00:00'
},
{
'name': 'embargo',
'hasStartDate': true,
'hasEndDate': false,
'maxStartDate': '2024-12-20T12:17:44.420+00:00'
},
{
'name': 'administrator',
'hasStartDate': false,
'hasEndDate': false
}
],
'type': 'submissionaccessoption',
'_links': {
'self': {
'href': 'http://localhost:8080/server/api/config/submissionaccessoptions/AccessConditionDefaultConfiguration'
}
}
});
export function getSubmissionAccessesConfigService(): SubmissionFormsConfigService {
return jasmine.createSpyObj('SubmissionAccessesConfigService', {
findByHref: createSuccessfulRemoteDataObject$(configRes),
});
}

View File

@@ -0,0 +1,13 @@
import { SubmissionFormsModel } from 'src/app/core/config/models/config-submission-forms.model';
import { of as observableOf } from 'rxjs';
const dataRes = Object.assign(new SubmissionFormsModel(), {
'id': 'AccessConditionDefaultConfiguration',
'accessConditions': [],
});
export function getSectionAccessesService() {
return jasmine.createSpyObj('SectionAccessesService', {
getAccessesData: observableOf(dataRes),
});
}

View File

@@ -1654,7 +1654,7 @@ export const mockFileFormData = {
},
},
{
accessConditionGroup:{
accessConditionGroup: {
name: [
{
value: 'lease',
@@ -1723,3 +1723,94 @@ export const mockFileFormData = {
}
]
};
export const mockAccessesFormData = {
discoverable: true,
accessCondition: [
{
accessConditionGroup: {
name: [
{
value: 'openaccess',
language: null,
authority: null,
display: 'openaccess',
confidence: -1,
place: 0,
otherInformation: null
}
],
},
},
{
accessConditionGroup: {
name: [
{
value: 'lease',
language: null,
authority: null,
display: 'lease',
confidence: -1,
place: 0,
otherInformation: null
}
],
endDate: [
{
value: {
year: 2019,
month: 1,
day: 16
},
language: null,
authority: null,
display: {
year: 2019,
month: 1,
day: 16
},
confidence: -1,
place: 0,
otherInformation: null
}
],
}
},
{
accessConditionGroup: {
name: [
{
value: 'embargo',
language: null,
authority: null,
display: 'lease',
confidence: -1,
place: 0,
otherInformation: null
}
],
startDate: [
{
value: {
year: 2019,
month: 1,
day: 16
},
language: null,
authority: null,
display: {
year: 2019,
month: 1,
day: 16
},
confidence: -1,
place: 0,
otherInformation: null
}
],
}
}
]
};

View File

@@ -0,0 +1,106 @@
import { FormControl, FormGroup, AbstractControl } from '@angular/forms';
import { DynamicCheckboxModel, DynamicSelectModel } from '@ng-dynamic-forms/core';
export const accessConditionChangeEvent = {
$event: {
bubbles: true,
cancelBubble: false,
cancelable: false,
composed: false,
currentTarget: null,
defaultPrevented: false,
eventPhase: 0,
isTrusted: true,
path: ['input#accessCondition-0-endDate.form-control.ng-touched.ng-dirty.ng-valid', 'div.input-group.ng-touched.ng-valid.ng-pristine', 'ds-dynamic-date-picker-inline.ng-star-inserted, div, div, div, div.ng-touched.ng-valid.ng-pristine, ds-dynamic-form-control-container.col-6.ng-star-inserted, div#accessCondition-0-accessConditionGroup.form-row.ng-star-inserted.ng-touched.ng-valid.ng-pristine, ds-dynamic-form-group.ng-star-inserted, div, div, div, div.pl-1.pr-1.ng-touched.ng-valid.ng-pristine, ds-dynamic-form-control-container.form-group.flex-fill.access-condition-group.ng-star-inserted.ng-to…, div.cdk-drag.cdk-drag-handle.form-row.cdk-drag-disabled.ng-star-inserted.ng-touched.ng-valid.ng-pris…, div#cdk-drop-list-5.cdk-drop-list, div#accessCondition.ng-star-inserted.ng-touched.ng-valid.ng-pristine, ds-dynamic-form-array.ng-star-inserted, div, div, div, div.form-group.ng-touched.ng-valid.ng-pristine, ds-dynamic-form-control-container.ng-star-inserted, ds-dynamic-form.ng-touched.ng-valid.ng-pristine, form.form-horizontal.ng-touched.ng-valid.ng-pristine, div.container-fluid, ds-form.ng-star-inserted, ds-section-accesses.ng-star-inserted, div#sectionContent_AccessConditionDefaultConfiguration.ng-star-inserted, div.card-body, div#AccessConditionDefaultConfiguration.collapse.show.ng-star-inserted, div.card.ng-star-inserted, ngb-accordion.accordion, div#section_AccessConditionDefaultConfiguration.section-focus, ds-submission-section-container.ng-star-inserted, div.submission-form-content, div.container-fluid, ds-submission-form, div.submission-submit-container, ds-submission-edit.ng-star-inserted, ds-themed-submission-edit.ng-star-inserted, div.ng-tns-c392-0, main.main-content.ng-tns-c392-0, div.inner-wrapper.ng-tns-c392-0.ng-trigger.ng-trigger-slideSidebarPadding, div.outer-wrapper.ng-tns-c392-0.ng-star-inserted, ds-root.ng-tns-c392-0.ng-star-inserted, ds-themed-root, ds-app, body, html.wf-droidsans-n4-active.wf-active, document, Window'],
returnValue: true,
srcElement: 'input#accessCondition-0-endDate.form-control.ng-touched.ng-dirty.ng-valid',
target: 'input#accessCondition-0-endDate.form-control.ng-touched.ng-dirty.ng-valid',
timeStamp: 143042.8999999999,
type: 'change',
},
context: null,
control: new FormControl({
errors: null,
pristine: false,
status: 'VALID',
statusChanges: { _isScalar: false, observers: [], closed: false, isStopped: false, hasError: false },
touched: true,
value: { year: 2021, month: 12, day: 30 },
valueChanges: { _isScalar: false, observers: [], closed: false, isStopped: false, hasError: false },
_updateOn: 'change',
}),
group: new FormGroup({}),
model: new DynamicSelectModel({
additional: null,
asyncValidators: null,
controlTooltip: null,
errorMessages: { required: 'submission.sections.upload.form.date-required-until' },
hidden: false,
hint: null,
id: 'endDate',
label: 'submission.sections.upload.form.until-label',
labelTooltip: null,
name: 'endDate',
placeholder: 'Until',
prefix: null,
relations: [],
required: true,
suffix: null,
tabIndex: null,
updateOn: null,
validators: { required: null },
}),
type: 'change'
};
export const checkboxChangeEvent = {
$event: {
bubbles: true,
cancelBubble: false,
cancelable: false,
composed: false,
currentTarget: null,
defaultPrevented: false,
eventPhase: 0,
isTrusted: true,
path: ['input#discoverable.form-check-input.ng-valid.ng-touched.ng-pristine', 'label.form-check-label', 'div.form-check.ng-touched.ng-valid.ng-pristine', 'dynamic-ng-bootstrap-checkbox.ng-star-inserted', 'div', 'div', 'div', 'div.form-group.ng-touched.ng-valid.ng-pristine', 'ds-dynamic-form-control-container.ng-star-inserted', 'ds-dynamic-form.ng-touched.ng-valid.ng-pristine', 'form.form-horizontal.ng-touched.ng-valid.ng-pristine', 'div.container-fluid', 'ds-form.ng-star-inserted', 'ds-section-accesses.ng-star-inserted', 'div#sectionContent_AccessConditionDefaultConfiguration.ng-star-inserted', 'div.card-body', 'div#AccessConditionDefaultConfiguration.collapse.show.ng-star-inserted', 'div.card.ng-star-inserted', 'ngb-accordion.accordion', 'div#section_AccessConditionDefaultConfiguration.section-focus', 'ds-submission-section-container.ng-star-inserted', 'div.submission-form-content', 'div.container-fluid', 'ds-submission-form', 'div.submission-submit-container', 'ds-submission-edit.ng-star-inserted', 'ds-themed-submission-edit.ng-star-inserted', 'div.ng-tns-c392-0', 'main.main-content.ng-tns-c392-0', 'div.inner-wrapper.ng-tns-c392-0.ng-trigger.ng-trigger-slideSidebarPadding', 'div.outer-wrapper.ng-tns-c392-0.ng-star-inserted', 'ds-root.ng-tns-c392-0.ng-star-inserted', 'ds-themed-root', 'ds-app', 'body', 'html.wf-droidsans-n4-active.wf-active', 'document', 'Window'],
returnValue: true,
srcElement: 'input#discoverable.form-check-input.ng-valid.ng-touched.ng-pristine',
target: 'input#discoverable.form-check-input.ng-valid.ng-touched.ng-pristine',
timeStamp: 143042.8999999999,
type: 'change',
},
context: null,
control: new FormControl({
errors: null,
pristine: false,
status: 'VALID',
statusChanges: { _isScalar: false, observers: [], closed: false, isStopped: false, hasError: false },
touched: true,
value: { year: 2021, month: 12, day: 30 },
valueChanges: { _isScalar: false, observers: [], closed: false, isStopped: false, hasError: false },
_updateOn: 'change',
}),
group: new FormGroup({}),
model: new DynamicCheckboxModel({
additional: null,
asyncValidators: null,
controlTooltip: null,
errorMessages: null,
hidden: false,
hint: null,
id: 'discoverable',
indeterminate: false,
label: 'Discoverable',
labelPosition: null,
labelTooltip: null,
name: 'discoverable',
relations: [],
required: false,
tabIndex: null,
updateOn: null,
validators: { required: null },
}),
type: 'change'
};

View File

@@ -6,6 +6,7 @@ import { debounceTime, filter, switchMap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model';
import { WorkspaceitemSectionAccessesObject } from '../../core/submission/models/workspaceitem-section-accesses.model';
import { hasValue, isEmpty, isNotEmptyOperator, isNotNull } from '../../shared/empty.util';
import { SubmissionDefinitionsModel } from '../../core/config/models/config-submission-definitions.model';
import { SubmissionService } from '../submission.service';
@@ -97,13 +98,13 @@ export class SubmissionEditComponent implements OnDestroy, OnInit {
* @param {SubmissionJsonPatchOperationsService} submissionJsonPatchOperationsService
*/
constructor(private changeDetectorRef: ChangeDetectorRef,
private notificationsService: NotificationsService,
private route: ActivatedRoute,
private router: Router,
private itemDataService: ItemDataService,
private submissionService: SubmissionService,
private translate: TranslateService,
private submissionJsonPatchOperationsService: SubmissionJsonPatchOperationsService) {
private notificationsService: NotificationsService,
private route: ActivatedRoute,
private router: Router,
private itemDataService: ItemDataService,
private submissionService: SubmissionService,
private translate: TranslateService,
private submissionJsonPatchOperationsService: SubmissionJsonPatchOperationsService) {
}
/**
@@ -127,6 +128,8 @@ export class SubmissionEditComponent implements OnDestroy, OnInit {
this.collectionId = (submissionObjectRD.payload.collection as Collection).id;
this.selfUrl = submissionObjectRD.payload._links.self.href;
this.sections = submissionObjectRD.payload.sections;
this.itemLink$.next(submissionObjectRD.payload._links.item.href);
this.item = submissionObjectRD.payload.item;
this.submissionDefinition = (submissionObjectRD.payload.submissionDefinition as SubmissionDefinitionsModel);

View File

@@ -1,7 +1,7 @@
import { ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
import { Observable, of as observableOf, Subscription } from 'rxjs';
import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
import { distinctUntilChanged, filter, map, switchMap, tap } from 'rxjs/operators';
import { AuthService } from '../../core/auth/auth.service';
import { SubmissionDefinitionsModel } from '../../core/config/models/config-submission-definitions.model';
import { Collection } from '../../core/shared/collection.model';
@@ -131,6 +131,7 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy {
if ((changes.collectionId && this.collectionId) && (changes.submissionId && this.submissionId)) {
this.isActive = true;
// retrieve submission's section list
this.submissionSections = this.submissionService.getSubmissionObject(this.submissionId).pipe(
filter(() => this.isActive),
@@ -143,7 +144,9 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy {
} else {
return observableOf([]);
}
}));
}),
tap((res) => { console.log(res); }));
this.uploadEnabled$ = this.sectionsService.isSectionTypeAvailable(this.submissionId, SectionsType.Upload);
// check if is submission loading
@@ -230,6 +233,8 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy {
protected getSectionsList(): Observable<any> {
return this.submissionService.getSubmissionSections(this.submissionId).pipe(
filter((sections: SectionDataObject[]) => isNotEmpty(sections)),
map((sections: SectionDataObject[]) => sections));
map((sections: SectionDataObject[]) => sections
));
}
}

View File

@@ -125,8 +125,8 @@ export class SubmissionObjectEffects {
this.submissionService.getSubmissionObjectLinkName(),
action.payload.submissionId,
'sections').pipe(
map((response: SubmissionObject[]) => new SaveSubmissionFormSuccessAction(action.payload.submissionId, response, action.payload.isManual)),
catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId))));
map((response: SubmissionObject[]) => new SaveSubmissionFormSuccessAction(action.payload.submissionId, response, action.payload.isManual)),
catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId))));
}));
/**
@@ -139,8 +139,8 @@ export class SubmissionObjectEffects {
this.submissionService.getSubmissionObjectLinkName(),
action.payload.submissionId,
'sections').pipe(
map((response: SubmissionObject[]) => new SaveForLaterSubmissionFormSuccessAction(action.payload.submissionId, response)),
catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId))));
map((response: SubmissionObject[]) => new SaveForLaterSubmissionFormSuccessAction(action.payload.submissionId, response)),
catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId))));
}));
/**
@@ -179,8 +179,8 @@ export class SubmissionObjectEffects {
action.payload.submissionId,
'sections',
action.payload.sectionId).pipe(
map((response: SubmissionObject[]) => new SaveSubmissionSectionFormSuccessAction(action.payload.submissionId, response)),
catchError(() => observableOf(new SaveSubmissionSectionFormErrorAction(action.payload.submissionId))));
map((response: SubmissionObject[]) => new SaveSubmissionSectionFormSuccessAction(action.payload.submissionId, response)),
catchError(() => observableOf(new SaveSubmissionSectionFormErrorAction(action.payload.submissionId))));
}));
/**
@@ -202,16 +202,16 @@ export class SubmissionObjectEffects {
this.submissionService.getSubmissionObjectLinkName(),
action.payload.submissionId,
'sections').pipe(
map((response: SubmissionObject[]) => {
if (this.canDeposit(response)) {
return new DepositSubmissionAction(action.payload.submissionId);
} else {
this.notificationsService.warning(null, this.translate.get('submission.sections.general.sections_not_valid'));
return this.parseSaveResponse((currentState.submission as SubmissionState).objects[action.payload.submissionId],
response, action.payload.submissionId, currentState.forms);
}
}),
catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId))));
map((response: SubmissionObject[]) => {
if (this.canDeposit(response)) {
return new DepositSubmissionAction(action.payload.submissionId);
} else {
this.notificationsService.warning(null, this.translate.get('submission.sections.general.sections_not_valid'));
return this.parseSaveResponse((currentState.submission as SubmissionState).objects[action.payload.submissionId],
response, action.payload.submissionId, currentState.forms);
}
}),
catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId))));
}));
/**
@@ -310,13 +310,13 @@ export class SubmissionObjectEffects {
tap(() => this.notificationsService.error(null, this.translate.get('submission.sections.general.discard_error_notice'))));
constructor(private actions$: Actions,
private notificationsService: NotificationsService,
private operationsService: SubmissionJsonPatchOperationsService,
private sectionService: SectionsService,
private store$: Store<any>,
private submissionService: SubmissionService,
private submissionObjectService: SubmissionObjectDataService,
private translate: TranslateService) {
private notificationsService: NotificationsService,
private operationsService: SubmissionJsonPatchOperationsService,
private sectionService: SectionsService,
private store$: Store<any>,
private submissionService: SubmissionService,
private submissionObjectService: SubmissionObjectDataService,
private translate: TranslateService) {
}
/**

View File

@@ -1 +1,2 @@
<p>section-accesses works!</p>
<ds-form *ngIf="!!formModel" #formRef="formComponent" [formId]="formId" [formModel]="formModel" [displaySubmit]="false" [displayCancel]="false"
(submitForm)="onSubmit()" (dfChange)="onChange($event)"></ds-form>

View File

@@ -1,25 +1,144 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormService } from './../../../shared/form/form.service';
import { ComponentFixture, TestBed, inject } from '@angular/core/testing';
import { SubmissionSectionAccessesComponent } from './section-accesses.component';
import { SectionsService } from 'src/app/submission/sections/sections.service';
import { SectionsServiceStub } from 'src/app/shared/testing/sections-service.stub';
import { FormBuilderService } from 'src/app/shared/form/builder/form-builder.service';
import { getMockFormBuilderService } from 'src/app/shared/mocks/form-builder-service.mock';
import { SubmissionAccessesConfigService } from 'src/app/core/config/submission-accesses-config.service';
import { getSubmissionAccessesConfigService } from 'src/app/shared/mocks/section-accesses-config.service.mock';
import { SectionAccessesService } from 'src/app/submission/sections/accesses/section-accesses.service';
import { SectionFormOperationsService } from 'src/app/submission/sections/form/section-form-operations.service';
import { JsonPatchOperationsBuilder } from 'src/app/core/json-patch/builder/json-patch-operations-builder';
import { TranslateService, TranslateModule } from '@ngx-translate/core';
import { SubmissionJsonPatchOperationsService } from 'src/app/core/submission/submission-json-patch-operations.service';
import { getSectionAccessesService } from 'src/app/shared/mocks/section-accesses.service.mock';
import { getMockFormOperationsService } from 'src/app/shared/mocks/form-operations-service.mock';
import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock';
import { SubmissionJsonPatchOperationsServiceStub } from 'src/app/shared/testing/submission-json-patch-operations-service.stub';
import { BrowserModule, By } from '@angular/platform-browser';
import { Observable, of as observableOf, observable } from 'rxjs';
import { select, Store } from '@ngrx/store';
import { FormComponent } from 'src/app/shared/form/form.component';
import { DynamicFormControlModel, DynamicCheckboxModel, DynamicFormArrayGroupModel, DynamicSelectModel, DynamicDatePickerModel, DynamicFormArrayModel } from '@ng-dynamic-forms/core';
import { AppState } from 'src/app/app.reducer';
import { getMockFormService } from 'src/app/shared/mocks/form-service.mock';
import { mockAccessesFormData } from 'src/app/shared/mocks/submission.mock';
import { accessConditionChangeEvent, checkboxChangeEvent } from 'src/app/shared/testing/form-event.stub';
describe('SubmissionSectionAccessesComponent', () => {
let component: SubmissionSectionAccessesComponent;
let fixture: ComponentFixture<SubmissionSectionAccessesComponent>;
const sectionsServiceStub = new SectionsServiceStub();
// const pathCombiner = new JsonPatchOperationPathCombiner('sections', sectionId, 'files', fileIndex);
const builderService: FormBuilderService = getMockFormBuilderService();
const submissionAccessesConfigService = getSubmissionAccessesConfigService();
const sectionAccessesService = getSectionAccessesService();
const sectionFormOperationsService = getMockFormOperationsService();
const operationsBuilder = jasmine.createSpyObj('operationsBuilder', {
add: undefined,
remove: undefined,
replace: undefined,
});
let formService: any;
const storeStub = jasmine.createSpyObj('store', ['dispatch']);
const sectionData = {
header: 'submit.progressbar.accessCondition',
config: 'http://localhost:8080/server/api/config/submissionaccessoptions/AccessConditionDefaultConfiguration',
mandatory: true,
sectionType: 'accessCondition',
collapsed: false,
enabled: true,
data: {
discoverable: true,
accessConditions: []
},
errorsToShow: [],
serverValidationErrors: [],
isLoading: false,
isValid: true
};
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [SubmissionSectionAccessesComponent]
imports: [
BrowserModule,
TranslateModule.forRoot()
],
declarations: [SubmissionSectionAccessesComponent, FormComponent],
providers: [
{ provide: SectionsService, useValue: sectionsServiceStub },
{ provide: FormBuilderService, useValue: builderService },
{ provide: SubmissionAccessesConfigService, useValue: submissionAccessesConfigService },
{ provide: SectionAccessesService, useValue: sectionAccessesService },
{ provide: SectionFormOperationsService, useValue: sectionFormOperationsService },
{ provide: JsonPatchOperationsBuilder, useValue: operationsBuilder },
{ provide: TranslateService, useValue: getMockTranslateService() },
{ provide: FormService, useValue: getMockFormService() },
{ provide: Store, useValue: storeStub },
{ provide: SubmissionJsonPatchOperationsService, useValue: SubmissionJsonPatchOperationsServiceStub },
{ provide: 'sectionDataProvider', useValue: sectionData },
{ provide: 'submissionIdProvider', useValue: '1508' },
]
})
.compileComponents();
});
beforeEach(() => {
beforeEach(inject([Store], (store: Store<AppState>) => {
fixture = TestBed.createComponent(SubmissionSectionAccessesComponent);
component = fixture.componentInstance;
formService = TestBed.inject(FormService);
formService.validateAllFormFields.and.callFake(() => null);
formService.isValid.and.returnValue(observableOf(true));
formService.getFormData.and.returnValue(observableOf(mockAccessesFormData));
fixture.detectChanges();
});
}));
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have created formModel', () => {
expect(component.formModel).toBeTruthy();
});
it('should have formModel length should be 2', () => {
expect(component.formModel.length).toEqual(2);
});
it('formModel should have 1 model type checkbox and 1 model type array', () => {
expect(component.formModel[0] instanceof DynamicCheckboxModel).toBeTrue();
expect(component.formModel[1] instanceof DynamicFormArrayModel).toBeTrue();
});
it('formModel type array should have formgroup with 1 input and 2 datepickers', () => {
const formModel: any = component.formModel[1];
const formGroup = formModel.groupFactory()[0].group;
expect(formGroup[0] instanceof DynamicSelectModel).toBeTrue();
expect(formGroup[1] instanceof DynamicDatePickerModel).toBeTrue();
expect(formGroup[2] instanceof DynamicDatePickerModel).toBeTrue();
});
it('when checkbox changed it should call operationsBuilder replace function', () => {
component.onChange(checkboxChangeEvent);
fixture.detectChanges();
expect(operationsBuilder.replace).toHaveBeenCalled();
});
it('when dropdown select changed it should call operationsBuilder add function', () => {
component.onChange(accessConditionChangeEvent);
fixture.detectChanges();
expect(operationsBuilder.add).toHaveBeenCalled();
});
});

View File

@@ -1,20 +1,404 @@
import { Component, OnInit } from '@angular/core';
import { SectionAccessesService } from './section-accesses.service';
import { Component, OnInit, ChangeDetectorRef, Inject, ViewChild } from '@angular/core';
import { renderSectionFor } from 'src/app/submission/sections/sections-decorator';
import { SectionsType } from 'src/app/submission/sections/sections-type';
import { SubmissionService } from 'src/app/submission/submission.service';
import { SectionDataObject } from 'src/app/submission/sections/models/section-data.model';
import { SectionsService } from 'src/app/submission/sections/sections.service';
import { SectionModelComponent } from 'src/app/submission/sections/models/section.model';
import { Observable, of, Subscription, combineLatest as observableCombineLatest, BehaviorSubject, combineLatest } from 'rxjs';
import { DynamicFormControlModel, MATCH_ENABLED, OR_OPERATOR, DynamicSelectModel, DynamicDatePickerModel, DynamicFormGroupModel, DynamicFormArrayModel, DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER, DynamicCheckboxModel, DynamicFormControlEvent } from '@ng-dynamic-forms/core';
import { FormBuilderService } from 'src/app/shared/form/builder/form-builder.service';
import { findIndex, isEqual } from 'lodash';
import {
ACCESS_CONDITION_GROUP_CONFIG,
ACCESS_CONDITION_GROUP_LAYOUT,
ACCESS_CONDITIONS_FORM_ARRAY_CONFIG,
ACCESS_CONDITIONS_FORM_ARRAY_LAYOUT,
FORM_ACCESS_CONDITION_END_DATE_CONFIG,
FORM_ACCESS_CONDITION_END_DATE_LAYOUT,
FORM_ACCESS_CONDITION_START_DATE_CONFIG,
FORM_ACCESS_CONDITION_START_DATE_LAYOUT,
FORM_ACCESS_CONDITION_TYPE_CONFIG,
FORM_ACCESS_CONDITION_TYPE_LAYOUT,
ACCESS_FORM_CHECKBOX_LAYOUT
} from './section-accesses.model';
import { isNotEmpty, isNotUndefined, isUndefined, hasValue, isNotNull } from 'src/app/shared/empty.util';
import { WorkspaceitemSectionAccessesObject } from '../../../core/submission/models/workspaceitem-section-accesses.model';
import { SubmissionAccessesConfigService } from 'src/app/core/config/submission-accesses-config.service';
import { getFirstSucceededRemoteData } from 'src/app/core/shared/operators';
import { map, take, filter, mergeMap } from 'rxjs/operators';
import { FormComponent } from 'src/app/shared/form/form.component';
import { FormService } from 'src/app/shared/form/form.service';
import { JsonPatchOperationPathCombiner } from 'src/app/core/json-patch/builder/json-patch-operation-path-combiner';
import { SectionFormOperationsService } from 'src/app/submission/sections/form/section-form-operations.service';
import { JsonPatchOperationsBuilder } from 'src/app/core/json-patch/builder/json-patch-operations-builder';
import { AccessesConditionOption } from 'src/app/core/config/models/config-accesses-conditions-options.model';
import { TranslateService } from '@ngx-translate/core';
import { FormFieldPreviousValueObject } from 'src/app/shared/form/builder/models/form-field-previous-value-object';
import { environment } from 'src/environments/environment';
import { SubmissionObject } from 'src/app/core/submission/models/submission-object.model';
import { SubmissionJsonPatchOperationsService } from 'src/app/core/submission/submission-json-patch-operations.service';
import { dateToISOFormat } from 'src/app/shared/date.util';
@Component({
selector: 'ds-section-accesses',
templateUrl: './section-accesses.component.html',
styleUrls: ['./section-accesses.component.scss']
})
@renderSectionFor(SectionsType.Accesses)
export class SubmissionSectionAccessesComponent implements OnInit {
@renderSectionFor(SectionsType.AccessesCondition)
export class SubmissionSectionAccessesComponent extends SectionModelComponent {
// tslint:disable-next-line:no-empty
constructor() { }
/**
* The FormComponent reference
*/
@ViewChild('formRef') public formRef: FormComponent;
// tslint:disable-next-line:no-empty
ngOnInit(): void {
/**
* List of available access conditions that could be set to files
*/
public availableAccessConditionOptions: AccessesConditionOption[]; // List of accessConditions that an user can select
/**
* The form id
* @type {string}
*/
public formId: string;
/**
* The accesses metadata data
* @type {WorkspaceitemSectionAccessesObject}
*/
public accessesData: WorkspaceitemSectionAccessesObject;
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
protected subs: Subscription[] = [];
/**
* The collection name this submission belonging to
* @type {string}
*/
public collectionName: string;
/**
* Is the upload required
* @type {boolean}
*/
public required$ = new BehaviorSubject<boolean>(true);
/**
* The [[JsonPatchOperationPathCombiner]] object
* @type {JsonPatchOperationPathCombiner}
*/
protected pathCombiner: JsonPatchOperationPathCombiner;
/**
* A map representing all field prevous values
* @type {Map}
*/
protected previousValue: Map<string, number[]> = new Map();
/**
* A map representing all field on their way to be removed
* @type {Map}
*/
protected fieldsOnTheirWayToBeRemoved: Map<string, number[]> = new Map();
/**
* The form model
* @type {DynamicFormControlModel[]}
*/
formModel: DynamicFormControlModel[];
/**
* Initialize instance variables
*
* @param {SectionsService} sectionService
* @param {SectionDataObject} injectedSectionData
* @param {FormService} formService
* @param {JsonPatchOperationsBuilder} operationsBuilder
* @param {SectionFormOperationsService} formOperationsService
* @param {FormBuilderService} formBuilderService
* @param {SubmissionAccessesConfigService} accessesConfigService
* @param {SectionAccessesService} accessesService
* @param {SubmissionJsonPatchOperationsService} operationsService
* @param {string} injectedSubmissionId
*/
constructor(
protected sectionService: SectionsService,
private formBuilderService: FormBuilderService,
private accessesConfigService: SubmissionAccessesConfigService,
private accessesService: SectionAccessesService,
protected formOperationsService: SectionFormOperationsService,
protected operationsBuilder: JsonPatchOperationsBuilder,
private formService: FormService,
private translate: TranslateService,
private operationsService: SubmissionJsonPatchOperationsService,
@Inject('sectionDataProvider') public injectedSectionData: SectionDataObject,
@Inject('submissionIdProvider') public injectedSubmissionId: string) {
super(undefined, injectedSectionData, injectedSubmissionId);
}
/**
* Initialize all instance variables and retrieve collection default access conditions
*/
protected onSectionInit(): void {
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionData.id);
this.formId = this.formService.getUniqueId(this.sectionData.id);
const config$ = this.accessesConfigService.findByHref(this.sectionData.config, true, false).pipe(
getFirstSucceededRemoteData(),
map((config) => config.payload),
);
const accessData$ = this.accessesService.getAccessesData(this.submissionId, this.sectionData.id);
combineLatest(config$, accessData$).subscribe(([config, accessData]) => {
this.availableAccessConditionOptions = isNotEmpty(config.accessConditionOptions) ? config.accessConditionOptions : [];
this.accessesData = accessData;
this.formModel = this.buildFileEditForm();
});
}
/**
* Get section status
*
* @return Observable<boolean>
* the section status
*/
protected getSectionStatus(): Observable<boolean> {
console.log('Method not implemented.');
return of(true);
}
/**
* Initialize form model
*/
protected buildFileEditForm() {
const formModel: DynamicFormControlModel[] = [];
formModel.push(
new DynamicCheckboxModel({
id: 'discoverable',
label: this.translate.instant('submission.sections.accesses.form.discoverable-label'),
name: 'discoverable',
value: this.accessesData.discoverable
})
);
const accessConditionTypeModelConfig = Object.assign({}, FORM_ACCESS_CONDITION_TYPE_CONFIG);
const accessConditionsArrayConfig = Object.assign({}, ACCESS_CONDITIONS_FORM_ARRAY_CONFIG);
const accessConditionTypeOptions = [];
for (const accessCondition of this.availableAccessConditionOptions) {
accessConditionTypeOptions.push(
{
label: accessCondition.name,
value: accessCondition.name
}
);
}
accessConditionTypeModelConfig.options = accessConditionTypeOptions;
// Dynamically assign of relation in config. For startdate, endDate, groups.
const hasStart = [];
const hasEnd = [];
const hasGroups = [];
this.availableAccessConditionOptions.forEach((condition) => {
const showStart: boolean = condition.hasStartDate === true;
const showEnd: boolean = condition.hasEndDate === true;
const showGroups: boolean = showStart || showEnd;
if (showStart) {
hasStart.push({ id: 'name', value: condition.name });
}
if (showEnd) {
hasEnd.push({ id: 'name', value: condition.name });
}
if (showGroups) {
hasGroups.push({ id: 'name', value: condition.name });
}
});
const confStart = { relations: [{ match: MATCH_ENABLED, operator: OR_OPERATOR, when: hasStart }] };
const confEnd = { relations: [{ match: MATCH_ENABLED, operator: OR_OPERATOR, when: hasEnd }] };
accessConditionsArrayConfig.groupFactory = () => {
const type = new DynamicSelectModel(accessConditionTypeModelConfig, FORM_ACCESS_CONDITION_TYPE_LAYOUT);
const startDateConfig = Object.assign({}, FORM_ACCESS_CONDITION_START_DATE_CONFIG, confStart);
const endDateConfig = Object.assign({}, FORM_ACCESS_CONDITION_END_DATE_CONFIG, confEnd);
const startDate = new DynamicDatePickerModel(startDateConfig, FORM_ACCESS_CONDITION_START_DATE_LAYOUT);
const endDate = new DynamicDatePickerModel(endDateConfig, FORM_ACCESS_CONDITION_END_DATE_LAYOUT);
const accessConditionGroupConfig = Object.assign({}, ACCESS_CONDITION_GROUP_CONFIG);
accessConditionGroupConfig.group = [type];
if (hasStart.length > 0) { accessConditionGroupConfig.group.push(startDate); }
if (hasEnd.length > 0) { accessConditionGroupConfig.group.push(endDate); }
return [new DynamicFormGroupModel(accessConditionGroupConfig, ACCESS_CONDITION_GROUP_LAYOUT)];
};
// Number of access conditions blocks in form
accessConditionsArrayConfig.initialCount = isNotEmpty(this.accessesData.accessConditions) ? this.accessesData.accessConditions.length : 1;
formModel.push(
new DynamicFormArrayModel(accessConditionsArrayConfig, ACCESS_CONDITIONS_FORM_ARRAY_LAYOUT)
);
this.initModelData(formModel);
return formModel;
}
/**
* Initialize form model values
*
* @param formModel
* The form model
*/
public initModelData(formModel: DynamicFormControlModel[]) {
this.accessesData.accessConditions.forEach((accessCondition, index) => {
Array.of('name', 'startDate', 'endDate')
.filter((key) => accessCondition.hasOwnProperty(key) && isNotEmpty(accessCondition[key]))
.forEach((key) => {
const metadataModel: any = this.formBuilderService.findById(key, formModel, index);
if (metadataModel) {
if (metadataModel.type === DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER) {
const date = new Date(accessCondition[key]);
metadataModel.value = {
year: date.getUTCFullYear(),
month: date.getUTCMonth() + 1,
day: date.getUTCDate()
};
} else {
metadataModel.value = accessCondition[key];
}
}
});
});
}
onSubmit() {
// this.formService.validateAllFormFields(this.formRef.formGroup);
}
/**
* Method called when a form dfChange event is fired.
* Dispatch form operations based on changes.
*/
onChange(event: DynamicFormControlEvent) {
if (event.model.type === 'CHECKBOX') {
const path = this.formOperationsService.getFieldPathSegmentedFromChangeEvent(event);
const value = this.formOperationsService.getFieldValueFromChangeEvent(event);
this.operationsBuilder.replace(this.pathCombiner.getPath(path), value.value, true);
} else {
// validate form
this.formService.validateAllFormFields(this.formRef.formGroup);
this.formService.isValid(this.formId).pipe(
take(1),
filter((isValid) => isValid),
mergeMap(() => this.formService.getFormData(this.formId)),
take(1),
mergeMap((formData: any) => {
console.log(formData);
const accessConditionsToSave = [];
formData.accessCondition
.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);
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. */
if (!accessConditionOpt.hasStartDate) {
delete currentAccessCondition.startDate;
} else if (accessCondition.startDate) {
const startDate = this.retrieveValueFromField(accessCondition.startDate);
currentAccessCondition.startDate = dateToISOFormat(startDate);
}
if (!accessConditionOpt.hasEndDate) {
delete currentAccessCondition.endDate;
} else if (accessCondition.endDate) {
const endDate = this.retrieveValueFromField(accessCondition.endDate);
currentAccessCondition.endDate = dateToISOFormat(endDate);
}
accessConditionsToSave.push(currentAccessCondition);
}
});
this.operationsBuilder.add(this.pathCombiner.getPath('accessConditions'), accessConditionsToSave, true);
return this.formService.getFormData(this.formId);
})
).subscribe((result: SubmissionObject[]) => {
console.log(result);
});
}
}
protected retrieveValueFromField(field: any) {
const temp = Array.isArray(field) ? field[0] : field;
return (temp) ? temp.value : undefined;
}
/**
* Check if the specified form field has already a value stored
*
* @param fieldId
* the section data retrieved from the serverù
* @param index
* the section data retrieved from the server
*/
hasStoredValue(fieldId, index): boolean {
if (isNotEmpty(this.sectionData.data)) {
return this.sectionData.data.hasOwnProperty(fieldId) &&
isNotEmpty(this.sectionData.data[fieldId][index]) &&
!this.isFieldToRemove(fieldId, index);
} else {
return false;
}
}
private hasRelatedCustomError(medatata): boolean {
const index = findIndex(this.sectionData.errorsToShow, { path: this.pathCombiner.getPath(medatata).path });
if (index !== -1) {
const error = this.sectionData.errorsToShow[index];
const validator = error.message.replace('error.validation.', '');
return !environment.form.validatorMap.hasOwnProperty(validator);
} else {
return false;
}
}
/**
* Check if the specified field is on the way to be removed
*
* @param fieldId
* the section data retrieved from the serverù
* @param index
* the section data retrieved from the server
*/
isFieldToRemove(fieldId, index) {
return this.fieldsOnTheirWayToBeRemoved.has(fieldId) && this.fieldsOnTheirWayToBeRemoved.get(fieldId).includes(index);
}
/**
* Unsubscribe from all subscriptions
*/
onSectionDestroy() {
this.subs
.filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe());
}
}

View File

@@ -0,0 +1,117 @@
import {
DynamicDatePickerModelConfig,
DynamicFormArrayModelConfig,
DynamicFormControlLayout,
DynamicFormGroupModelConfig,
DynamicSelectModelConfig,
MATCH_ENABLED,
OR_OPERATOR,
} from '@ng-dynamic-forms/core';
export const ACCESS_FORM_CHECKBOX_LAYOUT: DynamicFormControlLayout = {
element: {
host: 'form-group flex-fill access-condition-group',
id: 'discoverable',
// disabled: false,
label: 'submission.sections.accesses.form.discoverable-label',
name: 'discoverable',
}
};
export const ACCESS_CONDITION_GROUP_CONFIG: DynamicFormGroupModelConfig = {
id: 'accessConditionGroup',
group: []
};
export const ACCESS_CONDITION_GROUP_LAYOUT: DynamicFormControlLayout = {
element: {
host: 'form-group flex-fill access-condition-group',
container: 'pl-1 pr-1',
control: 'form-row '
}
};
export const ACCESS_CONDITIONS_FORM_ARRAY_CONFIG: DynamicFormArrayModelConfig = {
id: 'accessCondition',
groupFactory: null,
};
export const ACCESS_CONDITIONS_FORM_ARRAY_LAYOUT: DynamicFormControlLayout = {
grid: {
group: 'form-row',
}
};
export const FORM_ACCESS_CONDITION_TYPE_CONFIG: DynamicSelectModelConfig<any> = {
id: 'name',
label: 'submission.sections.upload.form.access-condition-label',
options: []
};
export const FORM_ACCESS_CONDITION_TYPE_LAYOUT: DynamicFormControlLayout = {
element: {
host: 'col-12',
label: 'col-form-label name-label'
}
};
export const FORM_ACCESS_CONDITION_START_DATE_CONFIG: DynamicDatePickerModelConfig = {
id: 'startDate',
label: 'submission.sections.upload.form.from-label',
placeholder: 'submission.sections.upload.form.from-placeholder',
inline: false,
toggleIcon: 'far fa-calendar-alt',
relations: [
{
match: MATCH_ENABLED,
operator: OR_OPERATOR,
when: []
}
],
required: true,
validators: {
required: null
},
errorMessages: {
required: 'submission.sections.upload.form.date-required-from'
}
};
export const FORM_ACCESS_CONDITION_START_DATE_LAYOUT: DynamicFormControlLayout = {
element: {
label: 'col-form-label'
},
grid: {
host: 'col-6'
}
};
export const FORM_ACCESS_CONDITION_END_DATE_CONFIG: DynamicDatePickerModelConfig = {
id: 'endDate',
label: 'submission.sections.upload.form.until-label',
placeholder: 'submission.sections.upload.form.until-placeholder',
inline: false,
toggleIcon: 'far fa-calendar-alt',
relations: [
{
match: MATCH_ENABLED,
operator: OR_OPERATOR,
when: []
}
],
required: true,
validators: {
required: null
},
errorMessages: {
required: 'submission.sections.upload.form.date-required-until'
}
};
export const FORM_ACCESS_CONDITION_END_DATE_LAYOUT: DynamicFormControlLayout = {
element: {
label: 'col-form-label'
},
grid: {
host: 'col-6'
}
};

View File

@@ -0,0 +1,44 @@
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { distinctUntilChanged, filter, map } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { SubmissionState } from '../../submission.reducers';
import { isUndefined } from 'util';
import { submissionSectionFromIdSelector, submissionSectionDataFromIdSelector } from 'src/app/submission/selectors';
/**
* A service that provides methods to handle submission's bitstream state.
*/
@Injectable()
export class SectionAccessesService {
/**
* Initialize service variables
*
* @param {Store<SubmissionState>} store
*/
constructor(private store: Store<SubmissionState>) { }
/**
* Return bitstream's metadata
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @param fileUUID
* The bitstream UUID
* @returns {Observable}
* Emits bitstream's metadata
*/
public getAccessesData(submissionId: string, sectionId: string): Observable<any> {
return this.store.select(submissionSectionDataFromIdSelector(submissionId, sectionId)).pipe(
filter((state) => !isUndefined(state)),
distinctUntilChanged());
}
}

View File

@@ -1,49 +1,33 @@
<div dsSection #sectionRef="sectionRef"
[attr.id]="'section_' + sectionData.id"
[ngClass]="{ 'section-focus' : sectionRef.isSectionActive() }"
[mandatory]="sectionData.mandatory"
[submissionId]="submissionId"
[sectionType]="sectionData.sectionType"
[sectionId]="sectionData.id">
<ngb-accordion #acc="ngbAccordion"
*ngIf="(sectionRef.isEnabled() | async)"
(panelChange)="sectionRef.sectionChange($event)"
activeIds="{{ sectionData.id }}"
[destroyOnHide]="false">
<div dsSection #sectionRef="sectionRef" [attr.id]="'section_' + sectionData.id" [ngClass]="{ 'section-focus' : sectionRef.isSectionActive() }"
[mandatory]="sectionData.mandatory" [submissionId]="submissionId" [sectionType]="sectionData.sectionType" [sectionId]="sectionData.id">
<!-- *ngIf="(sectionRef.isEnabled() | async)" -->
<ngb-accordion #acc="ngbAccordion" (panelChange)="sectionRef.sectionChange($event)" activeIds="{{ sectionData.id }}" [destroyOnHide]="false">
<ngb-panel id="{{ sectionData.id }}">
<ng-template ngbPanelTitle>
<span class="float-left section-title" tabindex="0">{{ 'submission.sections.'+sectionData.header | translate }}</span>
<div class="d-inline-block float-right">
<i *ngIf="!(sectionRef.isValid() | async) && !(sectionRef.hasErrors())" class="fas fa-exclamation-circle text-warning mr-3"
title="{{'submission.sections.status.warnings.title' | translate}}" role="img" [attr.aria-label]="'submission.sections.status.warnings.aria' | translate"></i>
<i *ngIf="(sectionRef.hasErrors())" class="fas fa-exclamation-circle text-danger mr-3"
title="{{'submission.sections.status.errors.title' | translate}}" role="img" [attr.aria-label]="'submission.sections.status.errors.aria' | translate"></i>
<i *ngIf="(sectionRef.isValid() | async) && !(sectionRef.hasErrors())" class="fas fa-check-circle text-success mr-3"
title="{{'submission.sections.status.valid.title' | translate}}" role="img" [attr.aria-label]="'submission.sections.status.valid.aria' | translate"></i>
<a class="close"
tabindex="0"
role="button"
[attr.aria-label]="(sectionRef.isOpen() ? 'submission.sections.toggle.aria.close' : 'submission.sections.toggle.aria.open') | translate: {sectionHeader: ('submission.sections.'+sectionData.header | translate)}"
[title]="(sectionRef.isOpen() ? 'submission.sections.toggle.close' : 'submission.sections.toggle.open') | translate">
title="{{'submission.sections.status.warnings.title' | translate}}" role="img" [attr.aria-label]="'submission.sections.status.warnings.aria' | translate"></i>
<i *ngIf="(sectionRef.hasErrors())" class="fas fa-exclamation-circle text-danger mr-3" title="{{'submission.sections.status.errors.title' | translate}}"
role="img" [attr.aria-label]="'submission.sections.status.errors.aria' | translate"></i>
<i *ngIf="(sectionRef.isValid() | async) && !(sectionRef.hasErrors())" class="fas fa-check-circle text-success mr-3" title="{{'submission.sections.status.valid.title' | translate}}"
role="img" [attr.aria-label]="'submission.sections.status.valid.aria' | translate"></i>
<a class="close" tabindex="0" role="button" [attr.aria-label]="(sectionRef.isOpen() ? 'submission.sections.toggle.aria.close' : 'submission.sections.toggle.aria.open') | translate: {sectionHeader: ('submission.sections.'+sectionData.header | translate)}"
[title]="(sectionRef.isOpen() ? 'submission.sections.toggle.close' : 'submission.sections.toggle.open') | translate">
<span *ngIf="sectionRef.isOpen()" class="fas fa-chevron-up fa-fw"></span>
<span *ngIf="!sectionRef.isOpen()" class="fas fa-chevron-down fa-fw"></span>
</a>
<a href="#" class="close mr-3" *ngIf="!sectionRef.isMandatory()"
(click)="removeSection($event)">
<a href="#" class="close mr-3" *ngIf="!sectionRef.isMandatory()" (click)="removeSection($event)">
<i class="fas fa-trash-o" aria-hidden="true" tabindex="0"></i>
</a>
</div>
</ng-template>
<ng-template ngbPanelContent>
<div id="sectionGenericError_{{sectionData.id}}" *ngIf="sectionRef.hasGenericErrors()">
<ds-alert *ngFor="let error of sectionRef.getErrors(); let i = index"
[content]="error"
[dismissible]="true"
[type]="AlertTypeEnum.Error"
(close)="sectionRef.removeError(i)"></ds-alert>
<ds-alert *ngFor="let error of sectionRef.getErrors(); let i = index" [content]="error" [dismissible]="true" [type]="AlertTypeEnum.Error"
(close)="sectionRef.removeError(i)"></ds-alert>
</div>
<div id="sectionContent_{{sectionData.id}}"
(click)="sectionRef.setFocus($event)">
<div id="sectionContent_{{sectionData.id}}" (click)="sectionRef.setFocus($event)">
<ng-container *ngComponentOutlet="getSectionContent(); injector: objectInjector;"></ng-container>
</div>
</ng-template>

View File

@@ -64,9 +64,9 @@ export class SubmissionSectionContainerComponent implements OnInit {
ngOnInit() {
this.objectInjector = Injector.create({
providers: [
{provide: 'collectionIdProvider', useFactory: () => (this.collectionId), deps: []},
{provide: 'sectionDataProvider', useFactory: () => (this.sectionData), deps: []},
{provide: 'submissionIdProvider', useFactory: () => (this.submissionId), deps: []},
{ provide: 'collectionIdProvider', useFactory: () => (this.collectionId), deps: [] },
{ provide: 'sectionDataProvider', useFactory: () => (this.sectionData), deps: [] },
{ provide: 'submissionIdProvider', useFactory: () => (this.submissionId), deps: [] },
],
parent: this.injector
});

View File

@@ -59,8 +59,8 @@ export abstract class SectionModelComponent implements OnDestroy, OnInit, Sectio
* @param {string} injectedSubmissionId
*/
public constructor(@Inject('collectionIdProvider') public injectedCollectionId: string,
@Inject('sectionDataProvider') public injectedSectionData: SectionDataObject,
@Inject('submissionIdProvider') public injectedSubmissionId: string) {
@Inject('sectionDataProvider') public injectedSectionData: SectionDataObject,
@Inject('submissionIdProvider') public injectedSubmissionId: string) {
this.collectionId = injectedCollectionId;
this.sectionData = injectedSectionData;
this.submissionId = injectedSubmissionId;

View File

@@ -5,5 +5,5 @@ export enum SectionsType {
License = 'license',
CcLicense = 'cclicense',
collection = 'collection',
Accesses = 'accesses',
AccessesCondition = 'accessCondition',
}

View File

@@ -174,10 +174,10 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit {
this.subscriptions.push(
this.uploadService
.getFileData(this.submissionId, this.sectionId, this.fileId).pipe(
filter((bitstream) => isNotUndefined(bitstream)))
filter((bitstream) => isNotUndefined(bitstream)))
.subscribe((bitstream) => {
this.fileData = bitstream;
}
this.fileData = bitstream;
}
)
);
}
@@ -251,12 +251,12 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit {
protected loadFormMetadata() {
this.configMetadataForm.rows.forEach((row) => {
row.fields.forEach((field) => {
field.selectableMetadata.forEach((metadatum) => {
this.formMetadata.push(metadatum.metadata);
});
row.fields.forEach((field) => {
field.selectableMetadata.forEach((metadatum) => {
this.formMetadata.push(metadatum.metadata);
});
}
});
}
);
}

View File

@@ -45,27 +45,28 @@ export function submissionUploadedFilesFromIdSelector(submissionId: string, sect
}
export function submissionUploadedFileFromUuidSelector(submissionId: string, sectionId: string, uuid: string): MemoizedSelector<SubmissionState, any> {
const filesSelector = submissionSectionDataFromIdSelector(submissionId, sectionId);
const filesSelector = submissionSectionDataFromIdSelector(submissionId, sectionId);
return keySelector<SubmissionState, any>(filesSelector, 'files', uuid);
}
export function submissionSectionFromIdSelector(submissionId: string, sectionId: string): MemoizedSelector<SubmissionState, any> {
const submissionIdSelector = submissionObjectFromIdSelector(submissionId);
const submissionIdSelector = submissionObjectFromIdSelector(submissionId);
return keySelector<SubmissionState, SubmissionObjectEntry>(submissionIdSelector, 'sections', sectionId);
}
export function submissionSectionDataFromIdSelector(submissionId: string, sectionId: string): MemoizedSelector<SubmissionState, any> {
const submissionIdSelector = submissionSectionFromIdSelector(submissionId, sectionId);
const submissionIdSelector = submissionSectionFromIdSelector(submissionId, sectionId);
return subStateSelector<SubmissionState, SubmissionSectionObject>(submissionIdSelector, 'data');
}
export function submissionSectionErrorsFromIdSelector(submissionId: string, sectionId: string): MemoizedSelector<SubmissionState, any> {
const submissionIdSelector = submissionSectionFromIdSelector(submissionId, sectionId);
const submissionIdSelector = submissionSectionFromIdSelector(submissionId, sectionId);
return subStateSelector<SubmissionState, SubmissionSectionObject>(submissionIdSelector, 'errorsToShow');
}
export function submissionSectionServerErrorsFromIdSelector(submissionId: string, sectionId: string): MemoizedSelector<SubmissionState, any> {
const submissionIdSelector = submissionSectionFromIdSelector(submissionId, sectionId);
const submissionIdSelector = submissionSectionFromIdSelector(submissionId, sectionId);
return subStateSelector<SubmissionState, SubmissionSectionObject>(submissionIdSelector, 'serverValidationErrors');
}

View File

@@ -37,6 +37,9 @@ import { ResearchEntitiesModule } from '../entity-groups/research-entities/resea
import { ThemedSubmissionEditComponent } from './edit/themed-submission-edit.component';
import { ThemedSubmissionSubmitComponent } from './submit/themed-submission-submit.component';
import { ThemedSubmissionImportExternalComponent } from './import-external/themed-submission-import-external.component';
import { SubmissionSectionAccessesComponent } from 'src/app/submission/sections/accesses/section-accesses.component';
import { SubmissionAccessesConfigService } from 'src/app/core/config/submission-accesses-config.service';
import { SectionAccessesService } from 'src/app/submission/sections/accesses/section-accesses.service';
const DECLARATIONS = [
SubmissionSectionUploadAccessConditionsComponent,
@@ -62,7 +65,8 @@ const DECLARATIONS = [
ThemedSubmissionImportExternalComponent,
SubmissionImportExternalSearchbarComponent,
SubmissionImportExternalPreviewComponent,
SubmissionImportExternalCollectionComponent
SubmissionImportExternalCollectionComponent,
SubmissionSectionAccessesComponent
];
@NgModule({
@@ -80,7 +84,9 @@ const DECLARATIONS = [
providers: [
SectionUploadService,
SectionsService,
SubmissionUploadsConfigService
SubmissionUploadsConfigService,
SubmissionAccessesConfigService,
SectionAccessesService
]
})

View File

@@ -3889,6 +3889,7 @@
"submission.sections.upload.upload-successful": "Upload successful",
"submission.sections.accesses.form.discoverable-label": "Discoverable",
"submission.submit.breadcrumbs": "New submission",