import { ChangeDetectorRef, Component, Inject, ViewChild } from '@angular/core'; import { DynamicFormControlEvent, DynamicFormControlModel } from '@ng-dynamic-forms/core'; import { Observable, Subscription } from 'rxjs'; import { distinctUntilChanged, filter, find, flatMap, map, take, tap } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { isEqual } from 'lodash'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; import { FormComponent } from '../../../shared/form/form.component'; import { FormService } from '../../../shared/form/form.service'; import { SectionModelComponent } from '../models/section.model'; import { SubmissionFormsConfigService } from '../../../core/config/submission-forms-config.service'; import { hasValue, isNotEmpty, isUndefined } from '../../../shared/empty.util'; import { ConfigData } from '../../../core/config/config-data'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { SubmissionFormsModel } from '../../../core/config/models/config-submission-forms.model'; import { SubmissionSectionError, SubmissionSectionObject } from '../../objects/submission-objects.reducer'; import { FormFieldPreviousValueObject } from '../../../shared/form/builder/models/form-field-previous-value-object'; import { GLOBAL_CONFIG } from '../../../../config'; import { GlobalConfig } from '../../../../config/global-config.interface'; import { SectionDataObject } from '../models/section-data.model'; import { renderSectionFor } from '../sections-decorator'; import { SectionsType } from '../sections-type'; import { SubmissionService } from '../../submission.service'; import { SectionFormOperationsService } from './section-form-operations.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { SectionsService } from '../sections.service'; import { difference } from '../../../shared/object.util'; import { WorkspaceitemSectionFormObject } from '../../../core/submission/models/workspaceitem-section-form.model'; /** * This component represents a section that contains a Form. */ @Component({ selector: 'ds-submission-section-form', styleUrls: ['./section-form.component.scss'], templateUrl: './section-form.component.html', }) @renderSectionFor(SectionsType.SubmissionForm) export class SubmissionSectionformComponent extends SectionModelComponent { /** * The form id * @type {string} */ public formId: string; /** * The form model * @type {DynamicFormControlModel[]} */ public formModel: DynamicFormControlModel[]; /** * A boolean representing if this section is updating * @type {boolean} */ public isUpdating = false; /** * A boolean representing if this section is loading * @type {boolean} */ public isLoading = true; /** * A map representing all field on their way to be removed * @type {Map} */ protected fieldsOnTheirWayToBeRemoved: Map = new Map(); /** * The form config * @type {SubmissionFormsModel} */ protected formConfig: SubmissionFormsModel; /** * The form data * @type {any} */ protected formData: any = Object.create({}); /** * The [JsonPatchOperationPathCombiner] object * @type {JsonPatchOperationPathCombiner} */ protected pathCombiner: JsonPatchOperationPathCombiner; /** * The [FormFieldPreviousValueObject] object * @type {FormFieldPreviousValueObject} */ protected previousValue: FormFieldPreviousValueObject = new FormFieldPreviousValueObject(); /** * The list of Subscription * @type {Array} */ protected subs: Subscription[] = []; /** * The FormComponent reference */ @ViewChild('formRef') private formRef: FormComponent; /** * Initialize instance variables * * @param {ChangeDetectorRef} cdr * @param {FormBuilderService} formBuilderService * @param {SectionFormOperationsService} formOperationsService * @param {FormService} formService * @param {SubmissionFormsConfigService} formConfigService * @param {NotificationsService} notificationsService * @param {SectionsService} sectionService * @param {SubmissionService} submissionService * @param {TranslateService} translate * @param {GlobalConfig} EnvConfig * @param {string} injectedCollectionId * @param {SectionDataObject} injectedSectionData * @param {string} injectedSubmissionId */ constructor(protected cdr: ChangeDetectorRef, protected formBuilderService: FormBuilderService, protected formOperationsService: SectionFormOperationsService, protected formService: FormService, protected formConfigService: SubmissionFormsConfigService, protected notificationsService: NotificationsService, protected sectionService: SectionsService, protected submissionService: SubmissionService, protected translate: TranslateService, @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, @Inject('collectionIdProvider') public injectedCollectionId: string, @Inject('sectionDataProvider') public injectedSectionData: SectionDataObject, @Inject('submissionIdProvider') public injectedSubmissionId: string) { super(injectedCollectionId, injectedSectionData, injectedSubmissionId); } /** * Initialize all instance variables and retrieve form configuration */ onSectionInit() { this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionData.id); this.formId = this.formService.getUniqueId(this.sectionData.id); this.formConfigService.getConfigByHref(this.sectionData.config).pipe( map((configData: ConfigData) => configData.payload), tap((config: SubmissionFormsModel) => this.formConfig = config), flatMap(() => this.sectionService.getSectionData(this.submissionId, this.sectionData.id)), take(1)) .subscribe((sectionData: WorkspaceitemSectionFormObject) => { if (isUndefined(this.formModel)) { this.sectionData.errors = []; // Is the first loading so init form this.initForm(sectionData); this.sectionData.data = sectionData; this.subscriptions(); this.isLoading = false; this.cdr.detectChanges(); } }) } /** * Unsubscribe from all subscriptions */ onSectionDestroy() { this.subs .filter((subscription) => hasValue(subscription)) .forEach((subscription) => subscription.unsubscribe()); } /** * Get section status * * @return Observable * the section status */ protected getSectionStatus(): Observable { return this.formService.isValid(this.formId); } /** * Check if the section data has been enriched by the server * * @param sectionData * the section data retrieved from the server */ hasMetadataEnrichment(sectionData: WorkspaceitemSectionFormObject): boolean { const diffResult = []; // compare current form data state with section data retrieved from store const diffObj = difference(sectionData, this.formData); // iterate over differences to check whether they are actually different Object.keys(diffObj) .forEach((key) => { diffObj[key].forEach((value) => { if (value.hasOwnProperty('value')) { diffResult.push(value); } }); }); return isNotEmpty(diffResult); } /** * Initialize form model * * @param sectionData * the section data retrieved from the server */ initForm(sectionData: WorkspaceitemSectionFormObject): void { try { this.formModel = this.formBuilderService.modelFromConfiguration( this.submissionId, this.formConfig, this.collectionId, sectionData, this.submissionService.getSubmissionScope()); } catch (e) { const msg: string = this.translate.instant('error.submission.sections.init-form-error') + e.toString(); const sectionError: SubmissionSectionError = { message: msg, path: '/sections/' + this.sectionData.id }; this.sectionService.setSectionError(this.submissionId, this.sectionData.id, sectionError); } } /** * Update form model * * @param sectionData * the section data retrieved from the server * @param errors * the section errors retrieved from the server */ updateForm(sectionData: WorkspaceitemSectionFormObject, errors: SubmissionSectionError[]): void { if (isNotEmpty(sectionData) && !isEqual(sectionData, this.sectionData.data)) { this.sectionData.data = sectionData; if (this.hasMetadataEnrichment(sectionData)) { const msg = this.translate.instant( 'submission.sections.general.metadata-extracted', { sectionId: this.sectionData.id }); this.notificationsService.info(null, msg, null, true); this.isUpdating = true; this.formModel = null; this.cdr.detectChanges(); this.initForm(sectionData); this.checksForErrors(errors); this.isUpdating = false; this.cdr.detectChanges(); } else if (isNotEmpty(errors) || isNotEmpty(this.sectionData.errors)) { this.checksForErrors(errors); } } else if (isNotEmpty(errors) || isNotEmpty(this.sectionData.errors)) { this.checksForErrors(errors); } } /** * Check if there are form validation error retrieved from server * * @param errors * the section errors retrieved from the server */ checksForErrors(errors: SubmissionSectionError[]): void { this.formService.isFormInitialized(this.formId).pipe( find((status: boolean) => status === true && !this.isUpdating)) .subscribe(() => { this.sectionService.checkSectionErrors(this.submissionId, this.sectionData.id, this.formId, errors, this.sectionData.errors); this.sectionData.errors = errors; this.cdr.detectChanges(); }); } /** * Initialize all subscriptions */ subscriptions(): void { this.subs.push( /** * Subscribe to form's data */ this.formService.getFormData(this.formId).pipe( distinctUntilChanged()) .subscribe((formData) => { this.formData = formData; }), /** * Subscribe to section state */ this.sectionService.getSectionState(this.submissionId, this.sectionData.id).pipe( filter((sectionState: SubmissionSectionObject) => { return isNotEmpty(sectionState) && (isNotEmpty(sectionState.data) || isNotEmpty(sectionState.errors)) }), distinctUntilChanged()) .subscribe((sectionState: SubmissionSectionObject) => { this.fieldsOnTheirWayToBeRemoved = new Map(); this.updateForm(sectionState.data as WorkspaceitemSectionFormObject, sectionState.errors); }) ) } /** * Method called when a form dfChange event is fired. * Dispatch form operations based on changes. * * @param event * the [[DynamicFormControlEvent]] emitted */ onChange(event: DynamicFormControlEvent): void { this.formOperationsService.dispatchOperationsFromEvent( this.pathCombiner, event, this.previousValue, this.hasStoredValue(this.formBuilderService.getId(event.model), this.formOperationsService.getArrayIndexFromEvent(event))); const metadata = this.formOperationsService.getFieldPathSegmentedFromChangeEvent(event); const value = this.formOperationsService.getFieldValueFromChangeEvent(event); if (this.EnvConfig.submission.autosave.metadata.indexOf(metadata) !== -1 && isNotEmpty(value)) { this.submissionService.dispatchSave(this.submissionId); } } /** * Method called when a form dfFocus event is fired. * Initialize [FormFieldPreviousValueObject] instance. * * @param event * the [[DynamicFormControlEvent]] emitted */ onFocus(event: DynamicFormControlEvent): void { const value = this.formOperationsService.getFieldValueFromChangeEvent(event); const path = this.formBuilderService.getPath(event.model); if (this.formBuilderService.hasMappedGroupValue(event.model)) { this.previousValue.path = path; this.previousValue.value = this.formOperationsService.getQualdropValueMap(event); } else if (isNotEmpty(value) && ((typeof value === 'object' && isNotEmpty(value.value)) || (typeof value === 'string'))) { this.previousValue.path = path; this.previousValue.value = value; } } /** * Method called when a form remove event is fired. * Dispatch form operations based on changes. * * @param event * the [[DynamicFormControlEvent]] emitted */ onRemove(event: DynamicFormControlEvent): void { const fieldId = this.formBuilderService.getId(event.model); const fieldIndex = this.formOperationsService.getArrayIndexFromEvent(event); // Keep track that this field will be removed if (this.fieldsOnTheirWayToBeRemoved.has(fieldId)) { const indexes = this.fieldsOnTheirWayToBeRemoved.get(fieldId); indexes.push(fieldIndex); this.fieldsOnTheirWayToBeRemoved.set(fieldId, indexes); } else { this.fieldsOnTheirWayToBeRemoved.set(fieldId, [fieldIndex]); } this.formOperationsService.dispatchOperationsFromEvent( this.pathCombiner, event, this.previousValue, this.hasStoredValue(fieldId, fieldIndex)); } /** * 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; } } /** * 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); } }