import { AfterViewInit, Component, ViewChild } from '@angular/core'; import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, Params, Router } from '@angular/router'; import { DmpProfileFieldDataType } from '@app/core/common/enum/dmp-profile-field-type'; import { DmpProfileStatus } from '@app/core/common/enum/dmp-profile-status'; import { DmpProfileType } from '@app/core/common/enum/dmp-profile-type'; import { DmpProfile } from '@app/core/model/dmp-profile/dmp-profile'; import { DmpProfileService } from '@app/core/services/dmp/dmp-profile.service'; import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service'; import { EnumUtils } from '@app/core/services/utilities/enum-utils.service'; import { DmpProfileEditorModel, DmpProfileFieldEditorModel } from '@app/ui/admin/dmp-profile/editor/dmp-profile-editor.model'; import { DmpProfileExternalAutoCompleteFieldDataEditorModel } from '@app/ui/admin/dmp-profile/editor/external-autocomplete/dmp-profile-external-autocomplete-field-editor.model'; import { BreadcrumbItem } from '@app/ui/misc/breadcrumb/definition/breadcrumb-item'; import { BaseComponent } from '@common/base/base.component'; import { FormService } from '@common/forms/form-service'; import { ValidationErrorModel } from '@common/forms/validation/error-model/validation-error-model'; import { TranslateService } from '@ngx-translate/core'; import { environment } from 'environments/environment'; import * as FileSaver from 'file-saver'; import { Observable, of as observableOf } from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; import { ConfigurationService } from '@app/core/services/configuration/configuration.service'; import { HttpClient } from '@angular/common/http'; import { MatomoService } from '@app/core/services/matomo/matomo-service'; import { MatDialog } from '@angular/material/dialog'; import { ConfirmationDialogComponent } from '@common/modules/confirmation-dialog/confirmation-dialog.component'; import { MultipleAutoCompleteConfiguration } from '@app/library/auto-complete/multiple/multiple-auto-complete-configuration'; import { DatasetProfileModel } from '@app/core/model/dataset/dataset-profile'; import { DataTableRequest } from '@app/core/model/data-table/data-table-request'; import { DatasetProfileCriteria } from '@app/core/query/dataset-profile/dataset-profile-criteria'; import { DmpService } from '@app/core/services/dmp/dmp.service'; import { AvailableProfilesComponent } from '@app/ui/dmp/editor/available-profiles/available-profiles.component'; import { DatasetPreviewDialogComponent } from '@app/ui/dmp/dataset-preview/dataset-preview-dialog.component'; import { CdkDragDrop, CdkDropList, CdkDrag, moveItemInArray } from '@angular/cdk/drag-drop'; import { DmpBlueprint, DmpBlueprintDefinition, ExtraFieldType, FieldCategory, SystemFieldType } from '@app/core/model/dmp/dmp-blueprint/dmp-blueprint'; import { DescriptionTemplatesInSectionEditor, DmpBlueprintEditor, FieldInSectionEditor, SectionDmpBlueprintEditor } from './dmp-blueprint-editor.model'; import { Guid } from '@common/types/guid'; import { isNullOrUndefined } from '@app/utilities/enhancers/utils'; import { DmpBlueprintListing } from '@app/core/model/dmp/dmp-blueprint/dmp-blueprint-listing'; import { FormValidationErrorsDialogComponent } from '@common/forms/form-validation-errors-dialog/form-validation-errors-dialog.component'; @Component({ selector: 'app-dmp-profile-editor-component', templateUrl: 'dmp-profile-editor.component.html', styleUrls: ['./dmp-profile-editor.component.scss'] }) export class DmpProfileEditorComponent extends BaseComponent implements AfterViewInit { isNew = true; isClone = false; viewOnly = false; dmpProfileModel: DmpProfileEditorModel; dmpBlueprintModel: DmpBlueprintEditor; formGroup: FormGroup = null; host: string; dmpProfileId: string; breadCrumbs: Observable; dmpBlueprintsFormGroup: FormGroup = null; profilesAutoCompleteConfiguration: MultipleAutoCompleteConfiguration; fieldList = [ {label: 'Title', type: SystemFieldType.TEXT}, {label: 'Description', type: SystemFieldType.HTML_TEXT}, {label: 'Researchers', type: SystemFieldType.RESEARCHERS}, {label: 'Organizations', type: SystemFieldType.ORGANIZATIONS}, {label: 'Language', type: SystemFieldType.LANGUAGE}, {label: 'Contact', type: SystemFieldType.CONTACT}, {label: 'Funder', type: SystemFieldType.FUNDER}, {label: 'Grant', type: SystemFieldType.GRANT}, {label: 'Project', type: SystemFieldType.PROJECT}, {label: 'License', type: SystemFieldType.LICENSE}, {label: 'Access Rights', type: SystemFieldType.ACCESS_RIGHTS} ]; systemFieldListPerSection: Array> = new Array(); descriptionTemplatesPerSection: Array> = new Array>(); constructor( private dmpProfileService: DmpProfileService, private _service: DmpService, private route: ActivatedRoute, private router: Router, private language: TranslateService, private enumUtils: EnumUtils, private uiNotificationService: UiNotificationService, private formService: FormService, private fb: FormBuilder, private configurationService: ConfigurationService, private httpClient: HttpClient, private matomoService: MatomoService, private dialog: MatDialog ) { super(); this.host = configurationService.server; } ngAfterViewInit() { this.matomoService.trackPageView('Admin: DMP Profile Edit'); this.profilesAutoCompleteConfiguration = { filterFn: this.filterProfiles.bind(this), initialItems: (excludedItems: any[]) => this.filterProfiles('').pipe(map(result => result.filter(resultItem => (excludedItems || []).map(x => x.id).indexOf(resultItem.id) === -1))), displayFn: (item) => item['label'], titleFn: (item) => item['label'], subtitleFn: (item) => item['description'], popupItemActionIcon: 'visibility' }; this.route.params .pipe(takeUntil(this._destroyed)) .subscribe((params: Params) => { this.dmpProfileId = params['id']; const cloneId = params['cloneid']; if (this.dmpProfileId != null) { this.isNew = false; this.dmpProfileService.getSingleBlueprint(this.dmpProfileId).pipe(map(data => data as DmpBlueprint)) .pipe(takeUntil(this._destroyed)) .subscribe(data => { this.dmpBlueprintModel = new DmpBlueprintEditor().fromModel(data); this.formGroup = this.dmpBlueprintModel.buildForm(); this.buildSystemFields(); this.fillDescriptionTemplatesInMultAutocomplete(); if (this.dmpBlueprintModel.status == DmpProfileStatus.Finalized) { this.formGroup.disable(); this.viewOnly = true } this.breadCrumbs = observableOf([{ parentComponentName: 'DmpProfileListingComponent', label: this.language.instant('NAV-BAR.TEMPLATE'), url: '/dmp-profiles/' + this.dmpProfileId }]); }); } else if (cloneId != null) { this.isClone = true; this.dmpProfileService.clone(cloneId).pipe(map(data => data as DmpBlueprint), takeUntil(this._destroyed)) .subscribe( data => { this.dmpBlueprintModel = new DmpBlueprintEditor().fromModel(data); this.dmpBlueprintModel.id = null; this.dmpBlueprintModel.created = null; this.dmpBlueprintModel.status = DmpProfileStatus.Draft; this.formGroup = this.dmpBlueprintModel.buildForm(); this.buildSystemFields(); this.fillDescriptionTemplatesInMultAutocomplete(); }, error => this.onCallbackError(error) ); } else { this.dmpProfileModel = new DmpProfileEditorModel(); this.dmpBlueprintModel = new DmpBlueprintEditor(); setTimeout(() => { // this.formGroup = this.dmpProfileModel.buildForm(); // this.addField(); this.dmpBlueprintModel.status = DmpProfileStatus.Draft; this.formGroup = this.dmpBlueprintModel.buildForm(); }); this.breadCrumbs = observableOf([{ parentComponentName: 'DmpProfileListingComponent', label: this.language.instant('NAV-BAR.TEMPLATE'), url: '/dmp-profiles/' + this.dmpProfileId }]); } }); } buildSystemFields(){ const sections = this.sectionsArray().controls.length; for(let i = 0; i < sections; i++){ let systemFieldsInSection = new Array(); this.fieldsArray(i).controls.forEach((field) => { if((field.get('category').value == FieldCategory.SYSTEM || field.get('category').value == 'SYSTEM')){ systemFieldsInSection.push(this.fieldList.find(f => f.type == field.get('type').value).type); } }) this.systemFieldListPerSection.push(systemFieldsInSection); } } fillDescriptionTemplatesInMultAutocomplete(){ const sections = this.sectionsArray().controls.length; for(let i = 0; i < sections; i++){ let descriptionTemplatesInSection = new Array(); this.descriptionTemplatesArray(i).controls.forEach((template) => { descriptionTemplatesInSection.push({id: template.value.descriptionTemplateId, label: template.value.label, description: ""}); }) this.descriptionTemplatesPerSection.push(descriptionTemplatesInSection); } } checkForProfiles(event, sectionIndex: number) { if (event.checked === false) { this.descriptionTemplatesPerSection[sectionIndex] = new Array(); this.descriptionTemplatesArray(sectionIndex).clear(); } } filterProfiles(value: string): Observable { const request = new DataTableRequest(null, null, { fields: ['+label'] }); const criteria = new DatasetProfileCriteria(); criteria.like = value; request.criteria = criteria; return this._service.searchDMPProfiles(request); } sectionsArray(): FormArray { //return this.dmpBlueprintsFormGroup.get('sections') as FormArray; return this.formGroup.get('definition').get('sections') as FormArray; } addSection(): void { const section: SectionDmpBlueprintEditor = new SectionDmpBlueprintEditor(); section.id = Guid.create().toString(); section.ordinal = this.sectionsArray().length + 1; section.hasTemplates = false; this.sectionsArray().push(section.buildForm()); this.systemFieldListPerSection.push(new Array()); this.descriptionTemplatesPerSection.push(new Array()); } removeSection(sectionIndex: number): void { this.systemFieldListPerSection.splice(sectionIndex, 1); this.descriptionTemplatesPerSection.splice(sectionIndex, 1); this.sectionsArray().removeAt(sectionIndex); this.sectionsArray().controls.forEach((section, index) => { section.get('ordinal').setValue(index + 1); }); } fieldsArray(sectionIndex: number): FormArray { return this.sectionsArray().at(sectionIndex).get('fields') as FormArray; } addField(sectionIndex: number, fieldCategory: FieldCategory, fieldType?: number): void { const field: FieldInSectionEditor = new FieldInSectionEditor(); field.id = Guid.create().toString(); field.ordinal = this.fieldsArray(sectionIndex).length + 1; field.category = fieldCategory; if(!isNullOrUndefined(fieldType)){ field.type = fieldType } field.required = (!isNullOrUndefined(fieldType) && (fieldType == 0 || fieldType == 1)) ? true : false; this.fieldsArray(sectionIndex).push(field.buildForm()); } removeField(sectionIndex: number, fieldIndex: number): void { this.fieldsArray(sectionIndex).removeAt(fieldIndex); } systemFieldsArray(sectionIndex: number): FormArray { return this.sectionsArray().at(sectionIndex).get('systemFields') as FormArray; } initSystemField(systemField?: SystemFieldType): FormGroup { return this.fb.group({ id: this.fb.control(Guid.create().toString()), type: this.fb.control(systemField), label: this.fb.control(''), placeholder: this.fb.control(''), description: this.fb.control(''), required: this.fb.control(true), ordinal: this.fb.control('') }); } addSystemField(sectionIndex: number, systemField?: SystemFieldType): void { this.addField(sectionIndex, FieldCategory.SYSTEM, systemField); } transfromEnumToString(type: SystemFieldType): string{ return this.fieldList.find(f => f.type == type).label; } selectedFieldType(type: SystemFieldType, sectionIndex: number): void { if (this.systemFieldDisabled(type, sectionIndex)) return; let index = this.systemFieldListPerSection[sectionIndex].indexOf(type); if (index == -1) { this.systemFieldListPerSection[sectionIndex].push(type); this.addSystemField(sectionIndex, type); } else { this.systemFieldListPerSection[sectionIndex].splice(index, 1); this.removeSystemField(sectionIndex, type); } } systemFieldDisabled(systemField: SystemFieldType, sectionIndex: number) { let i = 0; for (let s in this.sectionsArray().controls) { if (i != sectionIndex) { for (let f of this.fieldsArray(i).controls) { if ((f.get('category').value == FieldCategory.SYSTEM || f.get('category').value == 'SYSTEM') && f.get('type').value == systemField) { return true; } } } i++; } return false; } removeSystemFieldWithIndex(sectionIndex: number, fieldIndex: number): void { let type: SystemFieldType = this.fieldsArray(sectionIndex).at(fieldIndex).get('type').value; let index = this.systemFieldListPerSection[sectionIndex].indexOf(type); this.systemFieldListPerSection[sectionIndex] = this.systemFieldListPerSection[sectionIndex].filter(types => types != type); this.fieldsArray(sectionIndex).removeAt(fieldIndex); } removeSystemField(sectionIndex: number, systemField: SystemFieldType): void { let i = 0; for(let f of this.fieldsArray(sectionIndex).controls){ if((f.get('category').value == FieldCategory.SYSTEM || f.get('category').value == 'SYSTEM') && f.get('type').value == systemField){ this.fieldsArray(sectionIndex).removeAt(i); return; } i++; } } descriptionTemplatesArray(sectionIndex: number): FormArray { return this.sectionsArray().at(sectionIndex).get('descriptionTemplates') as FormArray; } addDescriptionTemplate(descriptionTemplate, sectionIndex: number): void { this.descriptionTemplatesArray(sectionIndex).push(this.fb.group({ label: this.fb.control(descriptionTemplate.value) })); } removeDescriptionTemplate(sectionIndex: number, templateIndex: number): void { this.descriptionTemplatesArray(sectionIndex).removeAt(templateIndex); } extraFieldsArray(sectionIndex: number): FormArray { return this.sectionsArray().at(sectionIndex).get('extraFields') as FormArray; } addExtraField(sectionIndex: number): void { this.addField(sectionIndex, FieldCategory.EXTRA); } removeExtraField(sectionIndex: number, fieldIndex: number): void { this.fieldsArray(sectionIndex).removeAt(fieldIndex); } getExtraFieldTypes(): Number[] { let keys: string[] = Object.keys(ExtraFieldType); keys = keys.slice(0, keys.length / 2); const values: Number[] = keys.map(Number); return values; } getExtraFieldTypeValue(extraFieldType: ExtraFieldType): string { switch (extraFieldType) { case ExtraFieldType.TEXT: return 'Text'; case ExtraFieldType.RICH_TEXT: return 'Rich Text'; case ExtraFieldType.DATE: return 'Date'; case ExtraFieldType.NUMBER: return 'Number'; } } drop(event: CdkDragDrop, sectionIndex: number) { moveItemInArray(this.fieldsArray(sectionIndex).controls, event.previousIndex, event.currentIndex); moveItemInArray(this.fieldsArray(sectionIndex).value, event.previousIndex, event.currentIndex); } dropSections(event: CdkDragDrop) { moveItemInArray(this.sectionsArray().controls, event.previousIndex, event.currentIndex); moveItemInArray(this.sectionsArray().value, event.previousIndex, event.currentIndex); this.sectionsArray().controls.forEach((section, index) => { section.get('ordinal').setValue(index + 1); }); moveItemInArray(this.systemFieldListPerSection, event.previousIndex, event.currentIndex); moveItemInArray(this.descriptionTemplatesPerSection, event.previousIndex, event.currentIndex); } moveItemInFormArray(formArray: FormArray, fromIndex: number, toIndex: number): void { const dir = toIndex > fromIndex ? 1 : -1; const item = formArray.at(fromIndex); for (let i = fromIndex; i * dir < toIndex * dir; i = i + dir) { const current = formArray.at(i + dir); formArray.setControl(i, current); } formArray.setControl(toIndex, item); } // clearForm(): void{ // this.dmpBlueprintsFormGroup.reset(); // } onRemoveTemplate(event, sectionIndex: number) { const profiles = this.descriptionTemplatesArray(sectionIndex).controls; const foundIndex = profiles.findIndex(profile => profile.get('descriptionTemplateId').value === event.id); foundIndex !== -1 && this.descriptionTemplatesArray(sectionIndex).removeAt(foundIndex); } onPreviewTemplate(event, sectionIndex: number) { const dialogRef = this.dialog.open(DatasetPreviewDialogComponent, { width: '590px', minHeight: '200px', restoreFocus: false, data: { template: event }, panelClass: 'custom-modalbox' }); dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(result => { if (result) { const profile: DescriptionTemplatesInSectionEditor = new DescriptionTemplatesInSectionEditor(); profile.id = Guid.create().toString(); profile.descriptionTemplateId = event.id; profile.label = event.label; this.descriptionTemplatesArray(sectionIndex).push(profile.buildForm()); const items = this.descriptionTemplatesPerSection[sectionIndex]; items.push({id: event.id, label: event.label, description: ""}); this.descriptionTemplatesPerSection[sectionIndex] = [...items]; } }); } onOptionSelected(item, sectionIndex){ const profile: DescriptionTemplatesInSectionEditor = new DescriptionTemplatesInSectionEditor(); profile.id = Guid.create().toString(); profile.descriptionTemplateId = item.id; profile.label = item.label; this.descriptionTemplatesArray(sectionIndex).push(profile.buildForm()); } checkValidity() { this.formService.touchAllFormFields(this.formGroup); if (!this.isFormValid()) { return false; } let errorMessages = []; if(!this.hasTitle()) { errorMessages.push("Title should be set."); } if(!this.hasDescription()) { errorMessages.push("Description should be set."); } if(!this.hasDescriptionTemplates()) { errorMessages.push("At least one section should have description templates."); } if(errorMessages.length > 0) { this.showValidationErrorsDialog(undefined, errorMessages); return false; } return true; } formSubmit(): void { if (this.checkValidity()) this.onSubmit(); } public isFormValid() { return this.formGroup.valid; } hasTitle(): boolean { const dmpBlueprint: DmpBlueprint = this.formGroup.value; return dmpBlueprint.definition.sections.some(section => section.fields.some(field => (field.category === FieldCategory.SYSTEM || field.category as unknown === 'SYSTEM') && field.type === SystemFieldType.TEXT)); } hasDescription(): boolean { const dmpBlueprint: DmpBlueprint = this.formGroup.value; return dmpBlueprint.definition.sections.some(section => section.fields.some(field => (field.category === FieldCategory.SYSTEM || field.category as unknown === 'SYSTEM') && field.type === SystemFieldType.HTML_TEXT)); } hasDescriptionTemplates(): boolean { const dmpBlueprint: DmpBlueprint = this.formGroup.value; return dmpBlueprint.definition.sections.some(section => section.hasTemplates == true); } private showValidationErrorsDialog(projectOnly?: boolean, errmess?: string[]) { const dialogRef = this.dialog.open(FormValidationErrorsDialogComponent, { disableClose: true, autoFocus: false, restoreFocus: false, data: { errorMessages:errmess, projectOnly: projectOnly }, }); } onSubmit(): void { this.dmpProfileService.createBlueprint(this.formGroup.value) .pipe(takeUntil(this._destroyed)) .subscribe( complete => this.onCallbackSuccess(), error => this.onCallbackError(error) ); } onCallbackSuccess(): void { this.uiNotificationService.snackBarNotification(this.isNew ? this.language.instant('GENERAL.SNACK-BAR.SUCCESSFUL-CREATION') : this.language.instant('GENERAL.SNACK-BAR.SUCCESSFUL-UPDATE'), SnackBarNotificationLevel.Success); this.router.navigate(['/dmp-profiles']); } onCallbackError(errorResponse: any) { this.setErrorModel(errorResponse.error); this.formService.validateAllFormFields(this.formGroup); } public setErrorModel(validationErrorModel: ValidationErrorModel) { Object.keys(validationErrorModel).forEach(item => { (this.dmpProfileModel.validationErrorModel)[item] = (validationErrorModel)[item]; }); } public cancel(): void { this.router.navigate(['/dmp-profiles']); } // addField() { // (this.formGroup.get('definition').get('fields')).push(new DmpProfileFieldEditorModel().buildForm()); // } // removeField(index: number) { // (this.formGroup.get('definition').get('fields')).controls.splice(index, 1); // } getDMPProfileFieldDataTypeValues(): Number[] { let keys: string[] = Object.keys(DmpProfileFieldDataType); keys = keys.slice(0, keys.length / 2); const values: Number[] = keys.map(Number); return values; } getDMPProfileFieldDataTypeWithLanguage(fieldType: DmpProfileFieldDataType): string { let result = ''; this.language.get(this.enumUtils.toDmpProfileFieldDataTypeString(fieldType)) .pipe(takeUntil(this._destroyed)) .subscribe((value: string) => { result = value; }); return result; } getDMPProfileFieldTypeValues(): Number[] { let keys: string[] = Object.keys(DmpProfileType); keys = keys.slice(0, keys.length / 2); const values: Number[] = keys.map(Number); return values; } getDMPProfileFieldTypeWithLanguage(profileType: DmpProfileType): string { let result = ''; this.language.get(this.enumUtils.toDmpProfileTypeString(profileType)) .pipe(takeUntil(this._destroyed)) .subscribe((value: string) => { result = value; }); return result; } delete() { this.dialog.open(ConfirmationDialogComponent,{data:{ isDeleteConfirmation: true, confirmButton: this.language.instant('DMP-PROFILE-EDITOR.CONFIRM-DELETE-DIALOG.CONFIRM-BUTTON'), cancelButton: this.language.instant("DMP-PROFILE-EDITOR.CONFIRM-DELETE-DIALOG.CANCEL-BUTTON"), message: this.language.instant("DMP-PROFILE-EDITOR.CONFIRM-DELETE-DIALOG.MESSAGE") }}) .afterClosed() .subscribe( confirmed =>{ if(confirmed){ if(this.formGroup.get('status').value == DmpProfileStatus.Draft) { this.formGroup.get('status').setValue(DmpProfileStatus.Deleted); this.dmpProfileService.createBlueprint(this.formGroup.value) .pipe(takeUntil(this._destroyed)) .subscribe( complete => this.onCallbackSuccess(), error => this.onCallbackError(error) ); } else { this.dmpProfileService.delete(this.dmpProfileId) .pipe(takeUntil(this._destroyed)) .subscribe( complete => this.onCallbackSuccess(), error => { if (error.error.statusCode == 674) { this.uiNotificationService.snackBarNotification(this.language.instant('GENERAL.SNACK-BAR.UNSUCCESSFUL-DMP-BLUEPRINT-DELETE'), SnackBarNotificationLevel.Error); } else { this.uiNotificationService.snackBarNotification(this.language.instant(error.message), SnackBarNotificationLevel.Error); } } ); } } } ) } finalize() { if (this.checkValidity()) { this.formGroup.get('status').setValue(DmpProfileStatus.Finalized); this.onSubmit(); } } downloadXML(): void { this.dmpProfileService.downloadXML(this.dmpProfileId) .pipe(takeUntil(this._destroyed)) .subscribe(response => { const blob = new Blob([response.body], { type: 'application/xml' }); const filename = this.getFilenameFromContentDispositionHeader(response.headers.get('Content-Disposition')); FileSaver.saveAs(blob, filename); }); } getFilenameFromContentDispositionHeader(header: string): string { const regex: RegExp = new RegExp(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/g); const matches = header.match(regex); let filename: string; for (let i = 0; i < matches.length; i++) { const match = matches[i]; if (match.includes('filename="')) { filename = match.substring(10, match.length - 1); break; } else if (match.includes('filename=')) { filename = match.substring(9); break; } } return filename; } isExternalAutocomplete(formGroup: FormGroup) { if (formGroup.get('dataType').value == DmpProfileFieldDataType.ExternalAutocomplete) { this.addControl(formGroup); return true; } else { this.removeControl(formGroup); return false; } } addControl(formGroup: FormGroup) { if (formGroup.get('dataType').value == 3) formGroup.addControl('externalAutocomplete', new DmpProfileExternalAutoCompleteFieldDataEditorModel().buildForm()); } removeControl(formGroup: FormGroup) { if (formGroup.get('dataType').value != 3) formGroup.removeControl('externalAutocomplete'); } }