import { Component, OnInit } from '@angular/core'; import { FormArray, UntypedFormGroup } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { DmpBlueprintService } from '@app/core/services/dmp/dmp-blueprint.service'; import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service'; import { EnumUtils } from '@app/core/services/utilities/enum-utils.service'; // import { BreadcrumbItem } from '@app/ui/misc/breadcrumb/definition/breadcrumb-item'; import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { DatePipe } from '@angular/common'; import { MatCheckboxChange } from '@angular/material/checkbox'; import { MatSelectChange } from '@angular/material/select'; import { DmpBlueprintFieldCategory } from '@app/core/common/enum/dmp-blueprint-field-category'; import { DmpBlueprintExtraFieldDataType } from '@app/core/common/enum/dmp-blueprint-field-type'; import { DmpBlueprintStatus } from '@app/core/common/enum/dmp-blueprint-status'; import { DmpBlueprintSystemFieldType } from '@app/core/common/enum/dmp-blueprint-system-field-type'; import { IsActive } from '@app/core/common/enum/is-active.enum'; import { AppPermission } from '@app/core/common/enum/permission.enum'; import { DescriptionTemplate } from '@app/core/model/description-template/description-template'; import { DmpBlueprint, DmpBlueprintPersist, NewVersionDmpBlueprintPersist, SystemFieldInSection } from '@app/core/model/dmp-blueprint/dmp-blueprint'; import { AuthService } from '@app/core/services/auth/auth.service'; import { DescriptionTemplateService } from '@app/core/services/description-template/description-template.service'; import { LoggingService } from '@app/core/services/logging/logging-service'; import { MatomoService } from '@app/core/services/matomo/matomo-service'; import { FileUtils } from '@app/core/services/utilities/file-utils.service'; import { QueryParamsService } from '@app/core/services/utilities/query-params.service'; import { SingleAutoCompleteConfiguration } from '@app/library/auto-complete/single/single-auto-complete-configuration'; import { BaseEditor } from '@common/base/base-editor'; import { FormService } from '@common/forms/form-service'; import { FormValidationErrorsDialogComponent } from '@common/forms/form-validation-errors-dialog/form-validation-errors-dialog.component'; import { ConfirmationDialogComponent } from '@common/modules/confirmation-dialog/confirmation-dialog.component'; import { HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service'; import { FilterService } from '@common/modules/text-filter/filter-service'; import { Guid } from '@common/types/guid'; import { TranslateService } from '@ngx-translate/core'; import * as FileSaver from 'file-saver'; import { map, takeUntil } from 'rxjs/operators'; import { DescriptionTemplatePreviewDialogComponent } from '../../description-template/description-template-preview/description-template-preview-dialog.component'; import { DmpBlueprintEditorModel } from './dmp-blueprint-editor.model'; import { DmpBlueprintEditorResolver } from './dmp-blueprint-editor.resolver'; import { DmpBlueprintEditorService } from './dmp-blueprint-editor.service'; import { ReferenceTypeService } from '@app/core/services/reference-type/reference-type.service'; import { SemanticsService } from '@app/core/services/semantic/semantics.service'; import { PrefillingSourceService } from '@app/core/services/prefilling-source/prefilling-source.service'; import { ConfigurationService } from '@app/core/services/configuration/configuration.service'; import { LockService } from '@app/core/services/lock/lock.service'; import { LockTargetType } from '@app/core/common/enum/lock-target-type'; @Component({ selector: 'app-dmp-blueprint-editor-component', templateUrl: 'dmp-blueprint-editor.component.html', styleUrls: ['./dmp-blueprint-editor.component.scss'], providers: [DmpBlueprintEditorService] }) export class DmpBlueprintEditorComponent extends BaseEditor implements OnInit { isNew = true; isClone = false; isNewVersion = false; isDeleted = false; formGroup: UntypedFormGroup = null; showInactiveDetails = false; hoveredSectionIndex:number = -1; hoveredDescriptionTemplateIndex:number = -1; dmpBlueprintSectionFieldCategory = DmpBlueprintFieldCategory; dmpBlueprintSystemFieldType = DmpBlueprintSystemFieldType; public dmpBlueprintSystemFieldTypeEnum = this.enumUtils.getEnumValues(DmpBlueprintSystemFieldType); dmpBlueprintExtraFieldDataType = DmpBlueprintExtraFieldDataType; public dmpBlueprintExtraFieldDataTypeEnum = this.enumUtils.getEnumValues(DmpBlueprintExtraFieldDataType); public dmpBlueprintFieldCategoryEnum = this.enumUtils.getEnumValues(DmpBlueprintFieldCategory); descriptionTempalteGroupSingleAutocompleteConfiguration: SingleAutoCompleteConfiguration = { initialItems: (data?: any) => this.descriptionTemplateService.query(this.descriptionTemplateService.buildDescriptionTempalteGroupAutocompleteLookup(null, null, null, this.getUsedDescriptionTemplateGroupIds())).pipe(map(x => x.items)), filterFn: (searchQuery: string, data?: any) => this.descriptionTemplateService.query(this.descriptionTemplateService.buildDescriptionTempalteGroupAutocompleteLookup(searchQuery, null, null, this.getUsedDescriptionTemplateGroupIds() ? this.getUsedDescriptionTemplateGroupIds() : null)).pipe(map(x => x.items)), getSelectedItem: (selectedItem: any) => this.descriptionTemplateService.query(this.descriptionTemplateService.buildDescriptionTempalteGroupAutocompleteLookup(null, null, [selectedItem])).pipe(map(x => x.items[0])), displayFn: (item: DescriptionTemplate) => item.label, titleFn: (item: DescriptionTemplate) => item.label, subtitleFn: (item: DescriptionTemplate) => item.description, valueAssign: (item: DescriptionTemplate) => item.groupId, popupItemActionIcon: 'visibility' } protected get canDelete(): boolean { return !this.isDeleted && !this.isNew && this.hasPermission(this.authService.permissionEnum.DeleteDmpBlueprint); } protected get canSave(): boolean { if (this.isDeleted || !this.hasPermission(this.authService.permissionEnum.EditDmpBlueprint)) return false; if (this.isNewVersion) return this.canCreateNewVersion; return !this.formGroup.disabled; } protected get canFinalize(): boolean { return !this.isNewVersion && !this.isDeleted && this.hasPermission(this.authService.permissionEnum.EditDmpBlueprint); } protected get canCreateNewVersion(): boolean { return this.isFinalized; } protected get isFinalized(): boolean { return this.editorModel.status == DmpBlueprintStatus.Finalized; } private hasPermission(permission: AppPermission): boolean { return this.authService.hasPermission(permission) || this.editorModel?.permissions?.includes(permission); } constructor( // BaseFormEditor injected dependencies protected dialog: MatDialog, protected language: TranslateService, protected formService: FormService, protected router: Router, protected uiNotificationService: UiNotificationService, protected httpErrorHandlingService: HttpErrorHandlingService, protected filterService: FilterService, protected datePipe: DatePipe, protected route: ActivatedRoute, protected queryParamsService: QueryParamsService, protected lockService: LockService, protected authService: AuthService, protected configurationService: ConfigurationService, // Rest dependencies. Inject any other needed deps here: public enumUtils: EnumUtils, private dmpBlueprintService: DmpBlueprintService, private logger: LoggingService, private dmpBlueprintEditorService: DmpBlueprintEditorService, private fileUtils: FileUtils, private matomoService: MatomoService, public descriptionTemplateService: DescriptionTemplateService, public referenceTypeService: ReferenceTypeService, public semanticsService: SemanticsService, public prefillingSourceService: PrefillingSourceService ) { super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService, lockService, authService, configurationService); } ngOnInit(): void { this.matomoService.trackPageView('Admin: DMP Blueprints'); super.ngOnInit(); this.initModelFlags(this.route.snapshot.data['action']); this.route.data.subscribe(d => { this.initModelFlags(d['action']); }); if ((this.formGroup.get('definition').get('sections') as FormArray).length == 0) { this.addSection(); } } private initModelFlags(action: string): void { if (action == 'clone') { this.isNew = false; this.isClone = true; this.isNewVersion = false; } else if (action == 'new-version') { this.isNew = false; this.isClone = false; this.isNewVersion = true; } else { this.isClone = false; this.isNewVersion = false; } } getItem(itemId: Guid, successFunction: (item: DmpBlueprint) => void) { this.dmpBlueprintService.getSingle(itemId, DmpBlueprintEditorResolver.lookupFields()) .pipe(map(data => data as DmpBlueprint), takeUntil(this._destroyed)) .subscribe( data => successFunction(data), error => this.onCallbackError(error) ); } prepareForm(data: DmpBlueprint) { try { this.editorModel = data ? new DmpBlueprintEditorModel().fromModel(data) : new DmpBlueprintEditorModel(); this.isDeleted = data ? data.isActive === IsActive.Inactive : false; this.buildForm(); if (data && data.id) this.checkLock(data.id, LockTargetType.DmpBlueprint, 'DMP-BLUEPRINT-EDITOR.LOCKED-DIALOG.TITLE', 'DMP-BLUEPRINT-EDITOR.LOCKED-DIALOG.MESSAGE'); } catch (error) { this.logger.error('Could not parse dmpBlueprint item: ' + data + error); this.uiNotificationService.snackBarNotification(this.language.instant('COMMONS.ERRORS.DEFAULT'), SnackBarNotificationLevel.Error); } } buildForm() { this.formGroup = this.editorModel.buildForm(null, this.isDeleted || !this.authService.hasPermission(AppPermission.EditDmpBlueprint)); this.dmpBlueprintEditorService.setValidationErrorModel(this.editorModel.validationErrorModel); if (this.isFinalized || this.isDeleted) { this.formGroup.disable(); } const action = this.route.snapshot.data['action']; if (action && action == 'new-version') { this.formGroup.enable(); } } refreshData(id?: Guid): void { this.getItem(id ?? this.editorModel.id, (data: DmpBlueprint) => this.prepareForm(data)); } refreshOnNavigateToData(id?: Guid): void { this.formGroup.markAsPristine(); if (this.isNew) { let route = []; route.push('/dmp-blueprints/' + id); this.router.navigate(route, { queryParams: { 'lookup': this.queryParamsService.serializeLookup(this.lookupParams), 'lv': ++this.lv }, replaceUrl: true, relativeTo: this.route }); } else { this.refreshData(id); } } persistEntity(onSuccess?: (response) => void): void { if ( this.isNewVersion == false) { const formData = this.formService.getValue(this.formGroup.value) as DmpBlueprintPersist; this.dmpBlueprintService.persist(formData) .pipe(takeUntil(this._destroyed)).subscribe( complete => onSuccess ? onSuccess(complete) : this.onCallbackSuccess(complete), error => this.onCallbackError(error) ); } else if (this.isNewVersion == true && this.isNew == false && this.isClone == false) { const formData = this.formService.getValue(this.formGroup.value) as NewVersionDmpBlueprintPersist; this.dmpBlueprintService.newVersion(formData) .pipe(takeUntil(this._destroyed)).subscribe( complete => onSuccess ? onSuccess(complete) : this.onCallbackSuccess(complete), error => this.onCallbackError(error) ); } } formSubmit(): void { this.formService.removeAllBackEndErrors(this.formGroup); this.formService.touchAllFormFields(this.formGroup); if (!this.isFormValid()) { return; } this.persistEntity(); } public delete() { const value = this.formGroup.value; if (value.id) { const dialogRef = this.dialog.open(ConfirmationDialogComponent, { maxWidth: '300px', data: { message: this.language.instant('GENERAL.CONFIRMATION-DIALOG.DELETE-ITEM'), confirmButton: this.language.instant('GENERAL.CONFIRMATION-DIALOG.ACTIONS.CONFIRM'), cancelButton: this.language.instant('GENERAL.CONFIRMATION-DIALOG.ACTIONS.CANCEL') } }); dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(result => { if (result) { this.dmpBlueprintService.delete(value.id).pipe(takeUntil(this._destroyed)) .subscribe( complete => this.cancel(), error => this.onCallbackError(error) ); } }); } } clearErrorModel() { this.editorModel.validationErrorModel.clear(); this.formService.validateAllFormFields(this.formGroup); } // // // Sections // // isSectionSelected(sectionId: number): boolean { return this.hoveredSectionIndex === sectionId; } onSectionHover(sectionId: any): void { this.hoveredSectionIndex = sectionId; } clearHoveredSection(): void { this.hoveredSectionIndex = -1; } addSection(): void { const formArray = this.formGroup.get('definition').get('sections') as FormArray; formArray.push(this.editorModel.createChildSection(formArray.length)); this.addField(formArray.length - 1); } removeSection(sectionIndex: number): void { (this.formGroup.get('definition').get('sections') as FormArray).removeAt(sectionIndex); (this.formGroup.get('definition').get('sections') as FormArray).controls.forEach((section, index) => { section.get('ordinal').setValue(index + 1); }); //Reapply validators DmpBlueprintEditorModel.reApplySectionValidators( { formGroup: this.formGroup, validationErrorModel: this.editorModel.validationErrorModel } ); this.formGroup.get('definition').get('sections').markAsDirty(); } dropSections(event: CdkDragDrop) { const sectionsFormArray = (this.formGroup.get('definition').get('sections') as FormArray); moveItemInArray(sectionsFormArray.controls, event.previousIndex, event.currentIndex); sectionsFormArray.updateValueAndValidity(); sectionsFormArray.controls.forEach((section, index) => { section.get('ordinal').setValue(index + 1); }); DmpBlueprintEditorModel.reApplySectionValidators( { formGroup: this.formGroup, validationErrorModel: this.editorModel.validationErrorModel } ); this.formGroup.get('definition').get('sections').markAsDirty(); } // // // Fields // // addField(sectionIndex: number) { ((this.formGroup.get('definition').get('sections') as FormArray).at(sectionIndex).get('fields') as FormArray) .push(this.editorModel.createChildField(sectionIndex, ((this.formGroup.get('definition').get('sections') as FormArray).at(sectionIndex).get('fields') as FormArray).length)); } removeField(sectionIndex: number, fieldIndex: number): void { const formArray = ((this.formGroup.get('definition').get('sections') as FormArray).at(sectionIndex).get('fields') as FormArray); formArray.removeAt(fieldIndex); formArray.controls.forEach((section, index) => { section.get('ordinal').setValue(index + 1); }); //Reapply validators DmpBlueprintEditorModel.reApplySectionValidators( { formGroup: this.formGroup, validationErrorModel: this.editorModel.validationErrorModel } ); (this.formGroup.get('definition').get('sections') as FormArray).at(sectionIndex).get('fields').markAsDirty(); } systemFieldDisabled(systemField: DmpBlueprintSystemFieldType) { return (this.formGroup.get('definition').get('sections') as FormArray)?.controls.some(x => (x.get('fields') as FormArray).controls.some(y => (y as UntypedFormGroup).get('systemFieldType')?.value === systemField)); } fieldCategoryChanged(sectionIndex: number, fieldIndex: number) { //Reapply validators DmpBlueprintEditorModel.reApplySectionValidators( { formGroup: this.formGroup, validationErrorModel: this.editorModel.validationErrorModel } ); (this.formGroup.get('definition').get('sections') as FormArray).at(sectionIndex).get('fields').markAsDirty(); } dropFields(event: CdkDragDrop, sectionIndex: number) { const fieldsFormArray = ((this.formGroup.get('definition').get('sections') as FormArray).at(sectionIndex).get('fields') as FormArray); moveItemInArray(fieldsFormArray.controls, event.previousIndex, event.currentIndex); fieldsFormArray.updateValueAndValidity(); fieldsFormArray.controls.forEach((section, index) => { section.get('ordinal').setValue(index + 1); }); DmpBlueprintEditorModel.reApplySectionValidators({ formGroup: this.formGroup, validationErrorModel: this.editorModel.validationErrorModel } ); (this.formGroup.get('definition').get('sections') as FormArray).at(sectionIndex).get('fields').markAsDirty(); } //Description Templates isDescriptionTemplateSelected(descriptionTemplateId: number): boolean { return this.hoveredDescriptionTemplateIndex === descriptionTemplateId; } onDescriptionTemplateHover(descriptionTemplateId: any): void { this.hoveredDescriptionTemplateIndex = descriptionTemplateId; } clearHoveredDescriptionTemplate(): void { this.hoveredDescriptionTemplateIndex = -1; } removeAllDescriptionTemplates(matCheckBox: MatCheckboxChange, sectionIndex: number) { if (matCheckBox.checked == false) { const descriptionTemplateSize = ((this.formGroup.get('definition').get('sections') as FormArray).at(sectionIndex).get('descriptionTemplates') as FormArray).length; for (let i = 0; i < descriptionTemplateSize; i++) this.removeDescriptionTemplate(sectionIndex, 0); } } addDescriptionTemplate(sectionIndex: number): void { const descriptionTempaltesArray = (this.formGroup.get('definition').get('sections') as FormArray).at(sectionIndex).get('descriptionTemplates') as FormArray; descriptionTempaltesArray.push(this.editorModel.createChildDescriptionTemplate(sectionIndex, ((this.formGroup.get('definition').get('sections') as FormArray).at(sectionIndex).get('descriptionTemplates') as FormArray).length)); } getUsedDescriptionTemplateGroupIds(): Guid[] { let excludedGroupIds: Guid[] = []; (this.formGroup.get('definition').get('sections') as FormArray).controls.forEach((section, index) => { const descriptionTempaltesArray = (this.formGroup.get('definition').get('sections') as FormArray).at(index).get('descriptionTemplates') as FormArray; if (descriptionTempaltesArray.length > 1) { descriptionTempaltesArray.controls.forEach((template, index) => { if (template.get('descriptionTemplateGroupId').value != undefined) excludedGroupIds.push(template.get('descriptionTemplateGroupId').value as Guid); }) } }); return excludedGroupIds; } removeDescriptionTemplate(sectionIndex: number, descriptionTemplateIndex: number): void { const descriptionTempaltesArray = (this.formGroup.get('definition').get('sections') as FormArray).at(sectionIndex).get('descriptionTemplates') as FormArray; descriptionTempaltesArray.removeAt(descriptionTemplateIndex); DmpBlueprintEditorModel.reApplySectionValidators( { formGroup: this.formGroup, validationErrorModel: this.editorModel.validationErrorModel } ); descriptionTempaltesArray.markAsDirty(); } dropDescriptionTemplates(event: CdkDragDrop, sectionIndex: number) { const descriptionTemplatesFormArray = ((this.formGroup.get('definition').get('sections') as FormArray).at(sectionIndex).get('descriptionTemplates') as FormArray); moveItemInArray(descriptionTemplatesFormArray.controls, event.previousIndex, event.currentIndex); descriptionTemplatesFormArray.updateValueAndValidity(); DmpBlueprintEditorModel.reApplySectionValidators({ formGroup: this.formGroup, validationErrorModel: this.editorModel.validationErrorModel } ); (this.formGroup.get('definition').get('sections') as FormArray).at(sectionIndex).get('descriptionTemplates').markAsDirty(); } onPreviewDescriptionTemplate(event: DescriptionTemplate, sectionId: Guid) { const dialogRef = this.dialog.open(DescriptionTemplatePreviewDialogComponent, { width: '590px', minHeight: '200px', restoreFocus: false, data: { descriptionTemplateId: event.id }, panelClass: 'custom-modalbox' }); dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(result => { // if (result) { // let blueprints = this.sectionsArray().at(sectionIndex).get('descriptionTemplates').value;//this.formGroup.get('blueprints').value; // const blueprint: DescriptionTemplatesInSectionEditor = new DescriptionTemplatesInSectionEditor(); // blueprint.id = Guid.create().toString(); // blueprint.descriptionTemplateId = event.id; // blueprint.label = event.label; // blueprints.push(blueprint.buildForm()); // this.sectionsArray().at(sectionIndex).get('descriptionTemplates').setValue(blueprints);//this.formGroup.get('blueprints').setValue(blueprints); // this.blueprintsAutoCompleteConfiguration = { // 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' // }; // } }); } 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; } hasTitle(): boolean { const dmpBlueprint: DmpBlueprintPersist = this.formGroup.value; return dmpBlueprint.definition.sections.some(section => section.fields.some(field => (field.category == DmpBlueprintFieldCategory.System) && (field as SystemFieldInSection).systemFieldType === DmpBlueprintSystemFieldType.Title)); } hasDescription(): boolean { const dmpBlueprint: DmpBlueprintPersist = this.formGroup.value; return dmpBlueprint.definition.sections.some(section => section.fields.some(field => (field.category == DmpBlueprintFieldCategory.System) && (field as SystemFieldInSection).systemFieldType === DmpBlueprintSystemFieldType.Description)); } hasDescriptionTemplates(): boolean { const dmpBlueprint: DmpBlueprintPersist = 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 }, }); } public cancel(): void { this.router.navigate(['/dmp-blueprints']); } finalize() { if (this.checkValidity()) { this.formGroup.get('status').setValue(DmpBlueprintStatus.Finalized); this.formSubmit(); } } downloadXML(): void { const blueprintId = this.formGroup.get('id').value; if (blueprintId == null) return; this.dmpBlueprintService.downloadXML(blueprintId) .pipe(takeUntil(this._destroyed)) .subscribe(response => { const blob = new Blob([response.body], { type: 'application/xml' }); const filename = this.fileUtils.getFilenameFromContentDispositionHeader(response.headers.get('Content-Disposition')); FileSaver.saveAs(blob, filename); }); } }