import { Component, OnInit, QueryList, ViewChild } from '@angular/core'; import { FormArray, FormControl, UntypedFormArray, UntypedFormGroup } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; 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 { CdkStep, StepperSelectionEvent } from '@angular/cdk/stepper'; import { DatePipe } from '@angular/common'; import { MatStepper } from '@angular/material/stepper'; import { DescriptionTemplateStatus } from '@app/core/common/enum/description-template-status'; import { IsActive } from '@app/core/common/enum/is-active.enum'; import { AppPermission } from '@app/core/common/enum/permission.enum'; import { UserDescriptionTemplateRole } from '@app/core/common/enum/user-description-template-role'; import { DescriptionTemplate } from '@app/core/model/description-template/description-template'; import { DescriptionTemplatePersist, NewVersionDescriptionTemplatePersist } from '@app/core/model/description-template/description-template-persist'; import { LanguageInfo } from '@app/core/model/language-info'; import { User } from '@app/core/model/user/user'; import { AuthService } from '@app/core/services/auth/auth.service'; import { LanguageInfoService } from '@app/core/services/culture/language-info-service'; import { DescriptionTemplateTypeService } from '@app/core/services/description-template-type/description-template-type.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 { UserService } from '@app/core/services/user/user.service'; import { FileUtils } from '@app/core/services/utilities/file-utils.service'; import { QueryParamsService } from '@app/core/services/utilities/query-params.service'; import { BaseEditor } from '@common/base/base-editor'; import { FormService } from '@common/forms/form-service'; 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 { map, takeUntil } from 'rxjs/operators'; import { GENERAL_ANIMATIONS, STEPPER_ANIMATIONS } from './animations/animations'; import { DescriptionTemplateEditorModel, DescriptionTemplateFieldEditorModel, DescriptionTemplateFieldSetEditorModel, DescriptionTemplatePageEditorModel, DescriptionTemplateSectionEditorModel, UserDescriptionTemplateEditorModel } from './description-template-editor.model'; import { DescriptionTemplateEditorResolver } from './description-template-editor.resolver'; import { DescriptionTemplateEditorService } from './description-template-editor.service'; import { NewEntryType, ToCEntry, ToCEntryType } from './table-of-contents/description-template-table-of-contents-entry'; 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-description-template-editor-component', templateUrl: 'description-template-editor.component.html', styleUrls: ['./description-template-editor.component.scss'], animations: [...STEPPER_ANIMATIONS, ...GENERAL_ANIMATIONS], providers: [DescriptionTemplateEditorService] }) export class DescriptionTemplateEditorComponent extends BaseEditor implements OnInit { @ViewChild('stepper') stepper: MatStepper; isNew = true; isDeleted = false; formGroup: UntypedFormGroup = null; item: DescriptionTemplate; showInactiveDetails = false; finalized: DescriptionTemplateStatus.Finalized; availableLanguages: LanguageInfo[] = this.languageInfoService.getLanguageInfoValues(); isNewVersion = false; isClone = false; steps: QueryList; toCEntries: ToCEntry[]; selectedTocEntry: ToCEntry; colorizeInvalid: boolean = false; tocEntryEnumValues = ToCEntryType; usersMap: Map = new Map(); userFormControl = new FormControl(); //Preview previewFieldSet: DescriptionTemplate = null; previewPropertiesFormGroup: UntypedFormGroup = null; protected get canDelete(): boolean { return !this.isDeleted && !this.isNew && this.hasPermission(this.authService.permissionEnum.DeleteDescriptionTemplate); } protected get canSave(): boolean { return !this.isDeleted && this.hasPermission(this.authService.permissionEnum.EditDescriptionTemplate); } protected get canFinalize(): boolean { return !this.isDeleted && this.hasPermission(this.authService.permissionEnum.EditDescriptionTemplate); } 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 descriptionTemplateService: DescriptionTemplateService, public descriptionTemplateTypeService: DescriptionTemplateTypeService, private logger: LoggingService, private descriptionTemplateEditorService: DescriptionTemplateEditorService, private fileUtils: FileUtils, private matomoService: MatomoService, private languageInfoService: LanguageInfoService, public userService: UserService ) { 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']); } 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: DescriptionTemplate) => void) { this.descriptionTemplateService.getSingle(itemId, DescriptionTemplateEditorResolver.lookupFields()) .pipe(map(data => data as DescriptionTemplate), takeUntil(this._destroyed)) .subscribe( data => successFunction(data), error => this.onCallbackError(error) ); } prepareForm(data: DescriptionTemplate) { try { this.editorModel = data ? new DescriptionTemplateEditorModel().fromModel(data) : new DescriptionTemplateEditorModel(); this.item = data; // Add user info to Map, to present them. (this.item?.users ?? []).forEach(obj => { this.usersMap.set(obj.user.id, obj.user); }); this.isDeleted = data ? data.isActive === IsActive.Inactive : false; this.buildForm(); if (data && data.id) this.checkLock(data.id, LockTargetType.DescriptionTemplate, 'DESCRIPTION-TEMPLATE-EDITOR.LOCKED-DIALOG.TITLE', 'DESCRIPTION-TEMPLATE-EDITOR.LOCKED-DIALOG.MESSAGE'); setTimeout(() => { this.steps = this.stepper.steps; }); this._initializeToCEntries(); } catch (error) { console.error(error); this.logger.error('Could not parse descriptionTemplate 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.EditDescriptionTemplate)); this.descriptionTemplateEditorService.setValidationErrorModel(this.editorModel.validationErrorModel); if (this.editorModel.status == DescriptionTemplateStatus.Finalized || this.isDeleted) { this.formGroup.disable(); } const action = this.route.snapshot.data['action']; if (action && action == 'new-version') { this.formGroup.enable(); } } refreshData(): void { this.getItem(this.editorModel.id, (data: DescriptionTemplate) => this.prepareForm(data)); } refreshOnNavigateToData(id?: Guid): void { this.formGroup.markAsPristine(); let route = []; if (id === null) { route.push('../..'); } else if (this.isNew) { route.push('../' + id); } else { route.push('..'); } this.router.navigate(route, { queryParams: { 'lookup': this.queryParamsService.serializeLookup(this.lookupParams), 'lv': ++this.lv }, replaceUrl: true, relativeTo: this.route }); } persistEntity(onSuccess?: (response) => void): void { if (this.isNewVersion == false){ const formData = this.formService.getValue(this.formGroup.value) as DescriptionTemplatePersist; this.descriptionTemplateService.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 NewVersionDescriptionTemplatePersist; this.descriptionTemplateService.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.descriptionTemplateService.delete(value.id).pipe(takeUntil(this._destroyed)) .subscribe( complete => this.onCallbackSuccess(), error => this.onCallbackError(error) ); } }); } } clearErrorModel() { this.editorModel.validationErrorModel.clear(); this.formService.validateAllFormFields(this.formGroup); } finalize() { this.formService.removeAllBackEndErrors(this.formGroup); this.formService.touchAllFormFields(this.formGroup); const dialogRef = this.dialog.open(ConfirmationDialogComponent, { restoreFocus: false, data: { message: this.language.instant('DESCRIPTION-OVERVIEW.FINALIZE-DIALOG.TITLE'), confirmButton: this.language.instant('DESCRIPTION-OVERVIEW.FINALIZE-DIALOG.CONFIRM'), cancelButton: this.language.instant('DESCRIPTION-OVERVIEW.FINALIZE-DIALOG.NEGATIVE'), isDeleteConfirmation: false } }); dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(result => { if (result) { this.formGroup.get('status').setValue(DescriptionTemplateStatus.Finalized); this.persistEntity(); }}); } // // // Description Template User // // addUser(user: User) { console.log(user); const userArray = (this.formGroup.get('users') as FormArray) const newUser: UserDescriptionTemplateEditorModel = new UserDescriptionTemplateEditorModel(this.editorModel.validationErrorModel); newUser.userId = user.id; newUser.role = UserDescriptionTemplateRole.Owner; this.usersMap.set(user.id, user); (this.formGroup.get('users') as FormArray).push(newUser.buildForm({ rootPath: 'users[' + userArray.length + '].' })); this.userFormControl.reset(); } removeUser(index: number) { (this.formGroup.get('users') as FormArray).controls.splice(index, 1); this.formGroup.get('users').updateValueAndValidity(); this.reaplyValidators(); this.formGroup.get('users').markAsDirty(); } verifyAndRemoveUser(index: number) { const dialogRef = this.dialog.open(ConfirmationDialogComponent, { restoreFocus: false, 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'), isDeleteConfirmation: true } }); dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(approve => { if (approve) { this.removeUser(index); } }); } // // // Stepper // // onMatStepperSelectionChange(event: StepperSelectionEvent) { if (event.selectedIndex === (this.steps.length - 1)) {//preview selected this.generatePreviewForm(); } else { // this.formGroup = null; } this.formGroup.markAsUntouched(); } isStepCompleted(stepIndex: number) { let stepCompleted = false; this.steps.forEach((step, index) => { if (stepIndex === index) { stepCompleted = step.completed; } }); return stepCompleted; } isStepUnlocked(stepIndex: number): boolean { if (stepIndex === 0) return true; if (stepIndex < 0) return false; //if previous step is valid then unlock let stepUnlocked: boolean = false; if (!this.isStepUnlocked(stepIndex - 1)) return false; this.steps.forEach((step, index) => { if (index + 1 == stepIndex) {//previous step if (step.completed) { stepUnlocked = true; } } }); return stepUnlocked; } validateStep(selectedIndex) { if (selectedIndex === 1) {//form description if (this.formGroup.invalid) { this.checkFormValidation(); } } } // // // Preview // // generatePreviewForm() { // const formValue: DescriptionTemplatePersist = this.formGroup.getRawValue(); // const fields: DescriptionTemplateField[] = formValue.fields.map(editorField => { // return { // id: editorField.id, // ordinal: editorField.ordinal, // numbering: '', // schematics: editorField.schematics, // defaultValue: editorField.defaultValue, // visibilityRules: editorField.visibilityRules, // validations: editorField.validations, // includeInExport: editorField.includeInExport, // data: editorField.data // } // }); // const fieldSet: DescriptionTemplateFieldSet = { // id: formValue.id, // ordinal: formValue.ordinal, // numbering: '', // title: formValue.title, // description: formValue.description, // extendedDescription: formValue.extendedDescription, // additionalInformation: formValue.additionalInformation, // multiplicity: { // max: formValue.multiplicity.max, min: formValue.multiplicity.min, // placeholder: formValue.multiplicity.placeholder, tableView: formValue.multiplicity.tableView // }, // hasCommentField: formValue.hasCommentField, // fields: fields // } // const mockDescription: Description = { // descriptionTemplate: { // definition: { // pages: [ // { // sections: [{ // fieldSets: [fieldSet] // }] // } // ] // } // } // } // const descriptionEditorModel = new DescriptionEditorModel().fromModel(mockDescription, mockDescription.descriptionTemplate); // this.previewPropertiesFormGroup = descriptionEditorModel.properties.fieldSets.get(fieldSet.id).buildForm() as UntypedFormGroup; // this.previewFieldSet = fieldSet; // let data = this.form.getRawValue(); // this.datasetProfileService.preview(data).subscribe(x => { // this.datasetWizardModel = new DatasetWizardEditorModel().fromModel({ // datasetProfileDefinition: x // }); // this.updateVisibilityRules(); // this.formGroup = this.datasetWizardModel.buildForm().get('datasetProfileDefinition'); // }); } // // // Table of Contents // // private _initializeToCEntries() { const tocentries = this.refreshToCEntries();//tocentries are sorted based on their ordinal value this._updateOrdinals(tocentries); if (tocentries && tocentries.length) { this.selectedTocEntry = tocentries[0]; } //Checking invalid visibilty RULES const fieldsetEntries = this._getAllFieldSets(this.toCEntries); const fieldSetHavingInvalidVisibilityRules: ToCEntry[] = fieldsetEntries .filter(entry => { const fieldsFormGroup = entry.form.get('fields'); const invalid = (fieldsFormGroup as UntypedFormArray).controls.filter(field => { return this.hasInvalidVisibilityRule(field as UntypedFormGroup); }); if (invalid && invalid.length) { return true; } return false; }); if (fieldSetHavingInvalidVisibilityRules.length) { const occurences = fieldSetHavingInvalidVisibilityRules.map(record => record.numbering).join(' , '); this.dialog.open(ConfirmationDialogComponent, { data: { message: this.language.instant('DESCRIPTION-TEMPLATE-EDITOR.ERRORS.INVALID-VISIBILITY-RULES.MESSAGE-START') + occurences + this.language.instant('DESCRIPTION-TEMPLATE-EDITOR.ERRORS.INVALID-VISIBILITY-RULES.MESSAGE-END'), confirmButton: this.language.instant('DESCRIPTION-TEMPLATE-EDITOR.ERRORS.INVALID-VISIBILITY-RULES.CONFIRM-YES'), cancelButton: this.language.instant('DESCRIPTION-TEMPLATE-EDITOR.ERRORS.INVALID-VISIBILITY-RULES.CONFIRM-NO') }, maxWidth: '30em' }) .afterClosed() .subscribe(confirm => { if (confirm) { this.removeFieldSetVisibilityRules(fieldSetHavingInvalidVisibilityRules); this.uiNotificationService.snackBarNotification(this.language.instant('DESCRIPTION-TEMPLATE-EDITOR.ERRORS.INVALID-VISIBILITY-RULES.REMOVE-SUCCESS'), SnackBarNotificationLevel.Success); } else { console.log('User not confirmed'); } }) } } private refreshToCEntries(): ToCEntry[] { this.toCEntries = this.getTocEntries(); //update selected tocentry if (this.selectedTocEntry) { this.selectedTocEntry = this._findTocEntryById(this.selectedTocEntry.id, this.toCEntries); } return this.toCEntries; } /** * Updates entries ordinal form value * based on the index they have on the tocentry array. * Tocentries that are on the same level have distinct ordinal value * * @param tocentries * */ private _updateOrdinals(tocentries: ToCEntry[]) { if (!tocentries || !tocentries.length) return; tocentries.forEach((e, idx) => { const ordinalControl = e.form.get('ordinal'); if (ordinalControl) { ordinalControl.setValue(idx); ordinalControl.updateValueAndValidity(); } this._updateOrdinals(e.subEntries); }); } getTocEntries(): ToCEntry[] { if (this.formGroup == null) { return []; } const result: ToCEntry[] = []; //build parent pages (this.formGroup.get('definition').get('pages') as UntypedFormArray).controls.forEach((pageElement, i) => { const page = { id: pageElement.get('id').value, label: pageElement.get('title').value, type: ToCEntryType.Page, form: pageElement, numbering: (i + 1).toString(), subEntriesType: ToCEntryType.Section, subEntries: [], validationRootPath: 'definition.pages[' + i + ']' } as ToCEntry; const subEntries = []; (pageElement.get('sections') as UntypedFormArray).controls.forEach((sectionElement, i) => { const item = { id: sectionElement.get('id').value, label: sectionElement.get('title').value, type: ToCEntryType.Section, form: sectionElement, numbering: page.numbering + '.' + (subEntries.length + 1), validationRootPath: page.validationRootPath + '.sections[' + i + ']' } as ToCEntry; const sectionItems = this.populateSections(sectionElement.get('sections') as UntypedFormArray, item.numbering, item.validationRootPath); const fieldSetItems = this.populateFieldSets(sectionElement.get('fieldSets') as UntypedFormArray, item.numbering, item.validationRootPath); if (sectionItems != null) { item.subEntries = sectionItems; item.subEntriesType = ToCEntryType.Section; } if (fieldSetItems != null) { if (item.subEntries == null) { item.subEntries = fieldSetItems; } else { item.subEntries.push(...fieldSetItems); } item.subEntriesType = ToCEntryType.FieldSet; } subEntries.push(item); }); page.subEntries = subEntries; result.push(page); }); this._sortToCentries(result);//ordeby ordinal this._updateNumbering(result, '');//update nubering if needed return result; } private populateSections(sections: UntypedFormArray, existingNumbering: string, validationRootPath: string): ToCEntry[] { if (sections == null || sections.controls == null || sections.controls.length == 0) { return null; } const result: ToCEntry[] = []; sections.controls.forEach((sectionElement, i) => { const item = { id: sectionElement.get('id').value, label: sectionElement.get('title').value, type: ToCEntryType.Section, form: sectionElement, numbering: existingNumbering + '.' + (i + 1), validationRootPath: validationRootPath + '.sections[' + i + ']' } as ToCEntry; const sectionItems = this.populateSections(sectionElement.get('sections') as UntypedFormArray, item.numbering, item.validationRootPath); const fieldSetItems = this.populateFieldSets(sectionElement.get('fieldSets') as UntypedFormArray, item.numbering, item.validationRootPath); if (sectionItems != null) { item.subEntries = sectionItems; item.subEntriesType = ToCEntryType.Section; } if (fieldSetItems != null) { if (item.subEntries == null) { item.subEntries = fieldSetItems; } else { item.subEntries.push(...fieldSetItems); } item.subEntriesType = ToCEntryType.FieldSet; } result.push(item); }); return result; } private populateFieldSets(fieldSets: UntypedFormArray, existingNumbering: string, validationRootPath: string): ToCEntry[] { if (fieldSets == null || fieldSets.controls == null || fieldSets.controls.length == 0) { return null; } const result: ToCEntry[] = []; fieldSets.controls.forEach((fieldSetElement, i) => { result.push({ id: fieldSetElement.get('id').value, label: fieldSetElement.get('title').value, type: ToCEntryType.FieldSet, //subEntries: this.populateSections((fieldSetElement.get('fieldSets') as FormArray), existingNumbering + '.' + i), form: fieldSetElement, numbering: existingNumbering + '.' + (i + 1), validationRootPath: validationRootPath } as ToCEntry) }); return result; } private _findTocEntryById(id: string, tocentries: ToCEntry[]): ToCEntry { if (!tocentries || !tocentries.length) { return null; } let tocEntryFound = tocentries.find(entry => entry.id === id); if (tocEntryFound) { return tocEntryFound; } for (let entry of tocentries) { const result = this._findTocEntryById(id, entry.subEntries); if (result) { tocEntryFound = result; break; } } return tocEntryFound ? tocEntryFound : null; } private reaplyValidators() { DescriptionTemplateEditorModel.reApplyDefinitionValidators( { formGroup: this.formGroup, validationErrorModel: this.editorModel.validationErrorModel } ); } addNewEntry(tce: NewEntryType) { const parent = tce.parent; const pages = this.formGroup.get('definition').get('pages') as FormArray; let pageIndex = -1; // define entry type switch (tce.childType) { case ToCEntryType.Page: const page: DescriptionTemplatePageEditorModel = new DescriptionTemplatePageEditorModel(this.editorModel.validationErrorModel); page.id = Guid.create().toString(); if (isNaN(pages.length)) { page.ordinal = 0; } else { page.ordinal = pages.length; } const pageForm = page.buildForm({ rootPath: 'definition.pages[' + pages.length + '].' }); console.log('definition.pages[' + pages.length + '].'); // this.dataModel.pages.push(page); pages.push(pageForm); // this.form.updateValueAndValidity(); this.refreshToCEntries(); this.selectedTocEntry = this._findTocEntryById(pageForm.get('id').value, this.toCEntries); break; case ToCEntryType.Section: const section: DescriptionTemplateSectionEditorModel = new DescriptionTemplateSectionEditorModel(this.editorModel.validationErrorModel); section.id = Guid.create().toString(); let sectionsArray: UntypedFormArray; if (parent.type === ToCEntryType.Page) {//FIRST LEVEL SECTION for (let i = 0; i < pages?.length; i++) { let page = pages.at(i); let pageId = page.get('id').value; if (pageId == parent.id) { pageIndex = i; break; } } sectionsArray = pages.controls.find(x => x.get('id')?.value === parent.id).get('sections') as UntypedFormArray; try { const max = sectionsArray.controls.map(control => control.get('ordinal').value) .reduce((a, b) => Math.max(a, b)); section.ordinal = max + 1; } catch { section.ordinal = sectionsArray.length; } sectionsArray.push(section.buildForm({ rootPath: 'definition.pages[' + pageIndex + '].sections[' + sectionsArray.length + '].' })); // this.form.updateValueAndValidity(); } else if (parent.type == ToCEntryType.Section) { //SUBSECTION OF SECTION let sectionIndexes: number[] = []; for (let j = 0; j < pages.length; j++) { const parentSections = pages.at(j).get('sections') as UntypedFormArray; sectionIndexes = this.findSectionIndex(parentSections, parent.id); if (sectionIndexes && sectionIndexes.length > 0) { pageIndex = j; break; } } let parentSectionRootPath = ''; if (sectionIndexes.length > 0) { sectionIndexes.forEach(index => { parentSectionRootPath = parentSectionRootPath + 'sections[' + index + '].' }); sectionsArray = parent.form.get('sections') as UntypedFormArray; //adding page parent MAYBE NOT NEEDED try { const maxOrdinal = sectionsArray.controls.map(control => control.get('ordinal').value).reduce((a, b) => Math.max(a, b)); section.ordinal = maxOrdinal + 1; } catch { section.ordinal = sectionsArray.length; } sectionsArray.push(section.buildForm({ rootPath: 'definition.pages[' + pageIndex + '].' + parentSectionRootPath + 'sections[' + sectionsArray.length + '].' })); // (child.form.parent as FormArray).push(section.buildForm()); } } else { console.error('Section can only be child of a page or another section'); } const sectionAdded = sectionsArray.at(sectionsArray.length - 1) as UntypedFormGroup; // sectionAdded.setValidators(this.customEditorValidators.sectionHasAtLeastOneChildOf('fieldSets','sections')); // sectionAdded.updateValueAndValidity(); this.refreshToCEntries(); this.selectedTocEntry = this._findTocEntryById(sectionAdded.get('id').value, this.toCEntries); break; case ToCEntryType.FieldSet: let sectionIndexes: number[] = []; for (let j = 0; j < pages.length; j++) { const parentSections = pages.at(j).get('sections') as UntypedFormArray; sectionIndexes = this.findSectionIndex(parentSections, parent.id); if (sectionIndexes && sectionIndexes.length > 0) { pageIndex = j; break; } } let parentSectionRootPath = ''; if (sectionIndexes.length > 0) { sectionIndexes.forEach(index => { parentSectionRootPath = parentSectionRootPath + 'sections[' + index + '].' }); } if (sectionIndexes.length > 0) { //create one field form fieldset const field: DescriptionTemplateFieldEditorModel = new DescriptionTemplateFieldEditorModel(this.editorModel.validationErrorModel); field.id = Guid.create().toString(); field.ordinal = 0;//first filed in the fields list const fieldSetsArray = parent.form.get('fieldSets') as UntypedFormArray const fieldForm = field.buildForm({ rootPath:'definition.pages[' + pageIndex + '].' + parentSectionRootPath + 'fieldSets[' + fieldSetsArray.length + '].' + 'fields[' + 0 + '].' }); //give fieldset id and ordinal const fieldSet: DescriptionTemplateFieldSetEditorModel = new DescriptionTemplateFieldSetEditorModel(this.editorModel.validationErrorModel); const fieldSetId = Guid.create().toString(); fieldSet.id = fieldSetId; try { const maxOrdinal = fieldSetsArray.controls.map(control => control.get('ordinal').value).reduce((a, b) => Math.max(a, b)); fieldSet.ordinal = maxOrdinal + 1; } catch { fieldSet.ordinal = fieldSetsArray.length; } const fieldsetForm = fieldSet.buildForm({ rootPath: 'definition.pages[' + pageIndex + '].' + parentSectionRootPath + 'fieldSets[' + fieldSetsArray.length + '].' }); (fieldsetForm.get('fields') as UntypedFormArray).push(fieldForm); fieldSetsArray.push(fieldsetForm); this.refreshToCEntries(); this.selectedTocEntry = this._findTocEntryById(fieldSetId.toString(), this.toCEntries); // fieldForm.updateValueAndValidity(); break; } default: break; } this.formGroup.updateValueAndValidity(); } private findSectionIndex(sectionFormArray: UntypedFormArray, parentId: string): number[] { for (let i = 0; i < sectionFormArray?.length; i++) { let sectionFormGroup = sectionFormArray.at(i); let sectionId = sectionFormGroup.get('id').value; const parentSections = sectionFormGroup.get('sections') as UntypedFormArray; if (sectionId == parentId) { return [i]; } else if (parentSections && parentSections.length > 0) { const indexes: number[] = this.findSectionIndex(parentSections, parentId); if (indexes && indexes.length > 0) { indexes.unshift(i); return indexes; } } } return null; } private getUpdatedSectionFormArray(sectionFormArray: UntypedFormArray, tceId: string): UntypedFormArray { for (let i = 0; i < sectionFormArray?.length; i++) { let sectionFormGroup = sectionFormArray.at(i); let sectionId = sectionFormGroup.get('id').value; const parentSections = sectionFormGroup.get('sections') as UntypedFormArray; if (sectionId == tceId) { sectionFormArray.removeAt(i); return sectionFormArray; // sectionFormArray.at(i).get('ordinal').patchValue(i); } else if (parentSections && parentSections.length > 0) { const currentSectionFormArray = this.getUpdatedSectionFormArray(parentSections, tceId); if (currentSectionFormArray != null || currentSectionFormArray != undefined) { return currentSectionFormArray; } } } return null; } onRemoveEntry(tce: ToCEntry) { const dialogRef = this.dialog.open(ConfirmationDialogComponent, { restoreFocus: false, 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'), isDeleteConfirmation: true } }); dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(result => { if (result) { this._deleteEntry(tce); } }); } private _deleteEntry(tce: ToCEntry) { const pages = this.formGroup.get('definition').get('pages') as UntypedFormArray; let pageIndex = -1; //define entry type switch (tce.type) { case ToCEntryType.Page: //get the index for (let i = 0; i < pages.length; i++) { let page = pages.at(i) as UntypedFormGroup; if (page.controls.id.value === tce.id) { pageIndex = i; break; } } if (pageIndex >= 0) { //remove page this._updateSelectedItem(tce); pages.removeAt(pageIndex); //update page ordinals for (let i = 0; i < pages.length; i++) { pages.at(i).get('ordinal').patchValue(i); } this.reaplyValidators(); //update validity // this.form.controls.sections.updateValueAndValidity(); } break; case ToCEntryType.Section: //FIRST LEVEL SECTION CASE let sectionIndex = -1; for (let j = 0; j < pages.length; j++) { const sections = pages.at(j).get('sections') as UntypedFormArray; for (let i = 0; i < sections?.length; i++) { let section = sections.at(i); let sectionId = section.get('id').value; if (sectionId == tce.id) { sectionIndex = i; pageIndex = j; break; } } } if (sectionIndex >= 0) { //section found const sections = pages.at(pageIndex).get('sections') as UntypedFormArray; //remove section // this._updateSelectedItem(tce); sections.removeAt(sectionIndex); //update ordinal for (let i = 0; i < sections.length; i++) { sections.at(i).get('ordinal').patchValue(i); } } else {//NOT FOUND IN FIRST LEVEL CASE //LOOK FOR SUBSECTION CASE // let parentSectionIndex = -1; // let sectionIndex = -1; // for (let j = 0; j < pages.length; j++) { // const parentSections = pages.at(j).get('sections') as UntypedFormArray; // for (let i = 0; i < parentSections?.length; i++) { // const sections = (pages.at(j).get('sections') as UntypedFormArray).at(i).get('sections') as UntypedFormArray; // for (let k = 0; i < sections?.length; i++) { // let section = sections.at(i); // let sectionId = section.get('id').value; // if (sectionId == tce.id) { // sectionIndex = k; // parentSectionIndex = i; // pageIndex = j; // break; // } // } // } // } // let parentFormArray = tce.form.parent as UntypedFormArray; // for (let i = 0; i < parentFormArray.length; i++) { // let section = parentFormArray.at(i); // if (section.get('id').value == tce.id) { // index = i; // break; // } // } // if (sectionIndex >= 0) { // this._updateSelectedItem(tce); // const parentFormArray = (pages.at(pageIndex).get('sections') as UntypedFormArray).at(parentSectionIndex).get('sections') as UntypedFormArray; // parentFormArray.removeAt(sectionIndex); // //update odrinal // for (let i = 0; i < parentFormArray.length; i++) { // parentFormArray.at(i).get('ordinal').patchValue(i); // } // } for (let j = 0; j < pages.length; j++) { const parentSections = pages.at(j).get('sections') as UntypedFormArray; const sectionFormArray = this.getUpdatedSectionFormArray(parentSections, tce.id); if (sectionFormArray) { //update ordinal for (let i = 0; i < sectionFormArray.length; i++) { sectionFormArray.at(i).get('ordinal').patchValue(i); } break; } } } this.reaplyValidators(); break; case ToCEntryType.FieldSet: const parentFormArray = tce.form.parent as UntypedFormArray; let idx = -1; for (let i = 0; i < parentFormArray.length; i++) { let inspectingField = parentFormArray.at(i); if (inspectingField.get('id').value === tce.id) { //fieldset found idx = i; break; } } if (idx >= 0) {//fieldset found this._updateSelectedItem(tce); parentFormArray.removeAt(idx); //patching order for (let i = 0; i < parentFormArray.length; i++) { parentFormArray.at(i).get('ordinal').patchValue(i); } this.reaplyValidators(); } break; default: break; } //in case selectedtocentrhy is child of the removed element // this.refreshToCEntries(); this.onDataNeedsRefresh(); this.formGroup.updateValueAndValidity(); } private _updateSelectedItem(tce: ToCEntry) { if (this.selectedTocEntry) { if (this.tocEntryIsChildOf(this.selectedTocEntry, tce)) { if (this.selectedTocEntry.type == ToCEntryType.Page) { this.selectedTocEntry = null; } else { const pages = this.formGroup.get('definition').get('pages') as UntypedFormArray; //if first level section // const firstLevelSections = (this.formGroup.get('definition').get('sections') as UntypedFormArray); // let isFirstLevel: boolean = false; // firstLevelSections.controls.forEach(section => { // if (section.get('id').value === tce.id) { // isFirstLevel = true; // } // }); let isFirstLevel: boolean = false; for (let j = 0; j < pages.length; j++) { const sections = pages.at(j).get('sections') as UntypedFormArray; for (let i = 0; i < sections?.length; i++) { let section = sections.at(i); let sectionId = section.get('id').value; if (sectionId == tce.id) { isFirstLevel = true; break; } } } let parentId = null; if (isFirstLevel) { parentId = tce.form.get('page').value; } else { parentId = tce.form.parent.parent.get('id').value } // const parentId = tce.form.parent.parent.get('id').value; if (parentId) { const tocentries = this.getTocEntries(); const parent = this._findTocEntryById(parentId, tocentries); if (parent) { this.selectedTocEntry = parent; } else { this.selectedTocEntry = null; } } else { this.selectedTocEntry = null; } } } } } tocEntryIsChildOf(testingChild: ToCEntry, parent: ToCEntry): boolean { if (!testingChild || !parent) return false; if (testingChild.id == parent.id) { return true; } if (parent.subEntries) { let childFound: boolean = false; parent.subEntries.forEach(subEntry => { if (this.tocEntryIsChildOf(testingChild, subEntry)) { childFound = true; return true; } }) return childFound; } return false; } onDataNeedsRefresh(params?) { const tocentries = this.refreshToCEntries(); if (params && params.draggedItemId) { if (params.draggedItemId) { this.displayItem(this._findTocEntryById(params.draggedItemId, tocentries)); } } this.formGroup.markAsDirty(); } displayItem(entry: ToCEntry): void { this.selectedTocEntry = entry; } /** * Get all filedsets in a tocentry array; * @param entries Tocentries to search in * @returns The tocentries that are Fieldsets provided in the entries */ private _getAllFieldSets(entries: ToCEntry[]): ToCEntry[] { const fieldsets: ToCEntry[] = []; if (!entries || !entries.length) return fieldsets; entries.forEach(e => { if (e.type === ToCEntryType.FieldSet) { fieldsets.push(e); } else { fieldsets.push(...this._getAllFieldSets(e.subEntries)); } }); return fieldsets; } private _sortToCentries(entries: ToCEntry[]) { if (!entries || !entries.length) return; entries.sort(this._compareOrdinals); entries.forEach(e => { this._sortToCentries(e.subEntries) }); } private _compareOrdinals(a, b) { const aValue = a.form.get('ordinal').value as number; const bValue = b.form.get('ordinal').value as number; // if(!aValue || !bValue) return 0; return aValue - bValue; } private _updateNumbering(entries: ToCEntry[], parentNumbering: string) { if (!entries || !entries.length) return; let prefix = ''; if (parentNumbering.length) { prefix = parentNumbering + '.'; } entries.forEach((entry, index) => { const numbering = prefix + (index + 1); entry.numbering = numbering; this._updateNumbering(entry.subEntries, numbering); }) } // // // Visibility Rules // // private hasInvalidVisibilityRule(field: UntypedFormGroup): boolean { // const renderStyle = field.get('viewStyle').get('renderStyle').value; // if (renderStyle && ![ // DatasetProfileFieldViewStyle.TextArea, // DatasetProfileFieldViewStyle.RichTextArea, // DatasetProfileFieldViewStyle.Upload, // DatasetProfileFieldViewStyle.FreeText, // DatasetProfileFieldViewStyle.BooleanDecision, // DatasetProfileFieldViewStyle.RadioBox, // DatasetProfileFieldViewStyle.CheckBox, // DatasetProfileFieldViewStyle.DatePicker, // DatasetProfileFieldViewStyle.ComboBox, // ].includes(renderStyle)) { // if (((renderStyle === DatasetProfileFieldViewStyle) && (field.get('data').get('type').value === DatasetProfileComboBoxType.Select))) { // return false; // } // try { // if (field.get('visible').get('rules').value.length) { // return true; // } // return false; // } catch { // return false; // } // } else { return false; // } } private removeFieldSetVisibilityRules(fieldsets: ToCEntry[]) { if (!fieldsets || !fieldsets.length) return; fieldsets.forEach(fieldset => { if (fieldset.type != ToCEntryType.FieldSet) { return; } const fields = fieldset.form.get('fields') as UntypedFormArray; fields.controls.forEach(fieldControl => { if (this.hasInvalidVisibilityRule(fieldControl as UntypedFormGroup)) { try { (fieldControl.get('visible').get('rules') as UntypedFormArray).clear(); } catch { } } }) }) } // // // Other // // scrollOnTop() { try { const topPage = document.getElementById('main-content'); topPage.scrollIntoView({ behavior: 'smooth' }); } catch (e) { console.log(e); console.log('coulnd not scroll'); } } get numOfPages() { return (this.formGroup.get('definition').get('pages'))?.length; } checkFormValidation() { this.colorizeInvalid = true; } get progressStyle() { // return {'transform': 'translateX('+this.barPercentage+'%) skewX(-25deg)'} const diff = 3; return { 'clip-path': `polygon(0 0, ${Math.round(this.barPercentage + diff)}% 0, ${Math.round(this.barPercentage)}% 100%, 0 100%)` } } get barPercentage() { if (!this.stepper || !this.steps) { return 0; } const selectedIndex = this.stepper.selectedIndex + 1; return (selectedIndex / this.stepper.steps.length) * 100; } public cancel(): void { this.router.navigate(['/description-templates']); } }