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; 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} ]; selectedSystemFields: string[] = []; 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']; 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 { 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); } } 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()); } removeSection(sectionIndex: number): void { this.sectionsArray().removeAt(sectionIndex); } 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(systemField: string, type: SystemFieldType, sectionIndex: number): void { let index = this.selectedSystemFields.indexOf(systemField); if (index == -1) { this.selectedSystemFields.push(systemField); this.addSystemField(sectionIndex, type); } else { this.selectedSystemFields.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 { 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); } 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) { // let profiles = this.sectionsArray().at(sectionIndex).get('descriptionTemplates').value;//this.formGroup.get('profiles').value; // const profile: DescriptionTemplatesInSectionEditor = new DescriptionTemplatesInSectionEditor(); // profile.id = Guid.create().toString(); // profile.descriptionTemplateId = event.id; // profile.label = event.label; // profiles.push(profile.buildForm()); // this.sectionsArray().at(sectionIndex).get('descriptionTemplates').setValue(profiles);//this.formGroup.get('profiles').setValue(profiles); // 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' // }; // } // }); // } 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 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 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){ 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) ); } } ) } 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'); } }