argos/dmp-frontend/src/app/ui/admin/dmp-blueprint/editor/dmp-blueprint-editor.compon...

571 lines
24 KiB
TypeScript

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<DmpBlueprintEditorModel, DmpBlueprint> 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>(DmpBlueprintSystemFieldType);
dmpBlueprintExtraFieldDataType = DmpBlueprintExtraFieldDataType;
public dmpBlueprintExtraFieldDataTypeEnum = this.enumUtils.getEnumValues<DmpBlueprintExtraFieldDataType>(DmpBlueprintExtraFieldDataType);
public dmpBlueprintFieldCategoryEnum = this.enumUtils.getEnumValues<DmpBlueprintFieldCategory>(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<string[]>) {
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<string[]>, 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<string[]>, 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);
});
}
}