import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core'; import { AbstractControl, Validators } from '@angular/forms'; import { ValidatorsService } from '@core/services/validators.service'; import { ReferenceFieldAPI } from '@core/typings/api/reference-fields.typing'; import { ReferenceFieldsUI } from '@core/typings/ui/reference-fields.typing'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { FormAudience } from '@features/configure-forms/form.typing'; import { CustomDataTablesService } from '@features/custom-data-tables/custom-data-table.service'; import { TypeaheadSelectOption, TypeSafeFormBuilder, TypeSafeFormGroup, TypeToken } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { Subscription } from 'rxjs'; import { CategoryGroup } from '../category-selector/category-selector.component'; import { ReferenceFieldsService } from '../services/reference-fields.service'; @Component({ selector: 'gc-create-edit-reference-field', templateUrl: './create-edit-reference-field.component.html', styleUrls: ['./create-edit-reference-field.component.scss'] }) export class CreateEditReferenceFieldComponent implements OnInit, OnChanges, OnDestroy { @Input() existingReferenceField: ReferenceFieldAPI.ReferenceFieldDisplayModel; @Input() isMerge = false; @Input() addingFieldToForm = false; @Input() forcedAudienceSelection: FormAudience = null; @Input() forceIsTableField = false; // From Table configuration add @Input() forceIsDataPoint = false; // from Data Set configuration add @Input() pendingKeyToBeCreated: string; @Input() uniqueKeys: string[]; @Input() existingKeys: string[]; @Input() lockFormatType: boolean; @Input() isViewOnly = false; @Input() lockCollectionType: boolean; @Input() tableImportNotAllowed: boolean; @Output() onFieldChange = new EventEmitter(); @Output() onValidityChange = new EventEmitter(); @Output() onAddingCategoryChange = new EventEmitter(); formGroup: TypeSafeFormGroup; sub = new Subscription(); existsOnForm = false; parentFieldExistsOnForm = false; addingCategory = false; typeOptions = this.referenceFieldService.getReferenceFieldTypeOptions(); aggregateOptions = this.referenceFieldService.getAggregateTypeOptions(); subsetCollectionOptions = this.referenceFieldService.getSubsetCollectionOptions(); supportsEncryption: boolean; supportsMasking: boolean; showSupportsMultiple: boolean; supportsTableField: boolean; aggregateFieldOptions: TypeaheadSelectOption[] = []; parentPicklistOptions: TypeaheadSelectOption[]; customDataTableOptions: TypeaheadSelectOption[]; ReferenceFieldTypes = ReferenceFieldsUI.ReferenceFieldTypes; duplicateAlert: string; FormAudience = FormAudience; canUpdateToSingleResponse = false; canUpdateCdtOnField = false; isExistingTableFieldOrDataPoint = false; ReferenceFieldsUI = ReferenceFieldsUI; formattingOptions: TypeaheadSelectOption[] = []; formAudienceOptions: TypeaheadSelectOption[] = [{ label: this.i18n.translate( 'common:lblApplicant', {}, 'Applicant' ), value: FormAudience.APPLICANT }, { label: this.i18n.translate( 'GLOBAL:textGrantManager', {}, 'Grant manager' ), value: FormAudience.MANAGER }]; subsetHelpText = this.i18n.translate( 'common:textSubsetHelpText2', {}, 'Combines related fields together into a meaningful unit' ); dataPointHelpText = this.i18n.translate( 'common:textDataPointHelpText', {}, 'Field that can be added to a field group' ); typeSelectHelp: string = null; fieldDetail: ReferenceFieldAPI.ReferenceFieldDetail; afterInit = false; $categoryGroup = new TypeToken>(); constructor ( private referenceFieldService: ReferenceFieldsService, private i18n: I18nService, private customDataTableService: CustomDataTablesService, private formBuilder: TypeSafeFormBuilder, private clientSettingsService: ClientSettingsService, private validatorService: ValidatorsService ) { } get isTypeTable () { return this.formGroup?.value?.type === ReferenceFieldsUI.ReferenceFieldTypes.Table; } get aggregateFieldParentExistsOnForm () { return this.parentFieldExistsOnForm && this.existingReferenceField?.type === ReferenceFieldsUI.ReferenceFieldTypes.Aggregate; } get isRootZone () { return this.clientSettingsService.clientSettings.isRootClient; } get isStandardProductField () { if (this.isRootZone) { // Standard Product Field limitations do not apply to the root zone return false; } return this.existingReferenceField?.isStandardProductField ?? false; } get audienceIsLocked () { return this.isStandardProductField || this.isViewOnly || this.existsOnForm || this.isMerge || this.isTypeTable || this.isExistingTableFieldOrDataPoint || !!this.forcedAudienceSelection; } get isTableFieldDisabled () { return this.isStandardProductField || this.isViewOnly || this.existsOnForm || this.isMerge || this.forceIsTableField || this.isExistingTableFieldOrDataPoint || this.addingFieldToForm; } get supportsMultipleDisabled () { /** Base Rules */ let isDisabled = this.isStandardProductField || this.isViewOnly || this.isMerge || this.isExistingTableFieldOrDataPoint; const isChecked = this.formGroup?.value.supportsMultiple; const hasData = this.fieldDetail?.hasResponses ?? false; const hasWflAutomation = this.fieldDetail?.hasWflAutomation ?? false; if (isChecked) { /** * If checked, all fields follow base rules * and you can't uncheck it unless there is no data */ isDisabled = isDisabled || hasData; return isDisabled; } else { /** * If unchecked, you can check it if: * Picklist: base rules + can't exist on a form * All other multi fields: base rules + has no WFL automation rules */ const isPicklist = this.formGroup?.value.type === ReferenceFieldsUI.ReferenceFieldTypes.CustomDataTable; if (isPicklist) { return isDisabled || this.existsOnForm; } else { return isDisabled || hasWflAutomation; } } } async ngOnInit () { this.aggregateFieldOptions = this.referenceFieldService.getApplicableAggregateFormFields(); this.existsOnForm = (this.existingReferenceField?.formCount || 0) > 0; const [ customDataTableOptions, canUpdateToSingleResponse, canUpdateCdtOnField, fieldDetail ] = await Promise.all([ this.setCustomDataTableOptions(), this.getCanUpdateToSingleResponse(), this.getCanUpdateCdtOnField(), this.referenceFieldService.getDetailForField( this.existingReferenceField?.referenceFieldId ) ]); this.customDataTableOptions = customDataTableOptions; this.canUpdateToSingleResponse = canUpdateToSingleResponse; this.canUpdateCdtOnField = canUpdateCdtOnField; this.fieldDetail = fieldDetail; this.setFormattingTypeOptions(); this.setParentReferenceField(); this.isExistingTableFieldOrDataPoint = this.setIsExistingTableFieldOrDataPoint(); const existingReferenceField: Partial = this.existingReferenceField || {}; this.setParentPicklistOptions(); const formAudience = existingReferenceField.formAudience ?? this.forcedAudienceSelection ?? FormAudience.APPLICANT; let isSingleResponse = existingReferenceField?.isSingleResponse ?? false; if (this.existingReferenceField?.referenceFieldId) { isSingleResponse = formAudience === FormAudience.APPLICANT ? true : existingReferenceField.isSingleResponse ?? false; } this.formGroup = this.formBuilder.group({ name: [ existingReferenceField.name || '', [Validators.required, Validators.maxLength(1000)] ], description: [ existingReferenceField.description || '', Validators.maxLength(1000) ], type: [existingReferenceField.type, Validators.required], formatType: [ existingReferenceField.formatType || ReferenceFieldAPI.ReferenceFieldFormattingType.NONE ], key: [ existingReferenceField.key || '', [ this.validatorService.getKeyValidator(this.uniqueKeys, this.existingKeys), Validators.maxLength(500) ] ], customDataTableGuid: [existingReferenceField.customDataTableGuid || ''], supportsMultiple: existingReferenceField.supportsMultiple ?? false, parentReferenceFieldId: [ existingReferenceField.parentReferenceFieldId ?? null ], categoryId: [ existingReferenceField.categoryId ?? null ], formAudience: [ formAudience, Validators.required ], aggregateType: [ existingReferenceField.aggregateType ?? ReferenceFieldAPI.ReferenceFieldAggregateType.Average ], isSingleResponse, isEncrypted: [ existingReferenceField.isEncrypted ?? false ], isMasked: [ existingReferenceField.isMasked ?? false ], isTableField: [ this.forceIsTableField ? true : (existingReferenceField?.isTableField ?? false) ], tableAllowsImport: [ this.tableImportNotAllowed ? false : (existingReferenceField.tableAllowsImport ?? false) ], aggregateTableReferenceFieldId: [ existingReferenceField.aggregateTableReferenceFieldId ?? null ], referenceFieldTableId: [ existingReferenceField.referenceFieldTableId ?? null ], standardComponentIsPublished: existingReferenceField.standardComponentIsPublished ?? false, isStandardProductField: existingReferenceField.isStandardProductField ?? false, subsetCollectionType: existingReferenceField?.subsetCollectionType || null }, { validators: [ this.forceSelectCustomDataTable(), this.forceSelectParentPicklistIfChild(), this.forceSelectParentFormFieldIfAggregate(), this.forceSelectDataSetCollectionType() ] }); this.filterTypeOptions(); if (this.forceIsTableField) { this.formAudienceOptions = this.formAudienceOptions.filter((opt) => { return opt.value === FormAudience.APPLICANT; }); } else if (this.forceIsDataPoint) { this.setTypeToDataPoint(); } this.setHelpers(); this.onFieldChange.emit(this.formGroup.value); this.onValidityChange.emit(this.formGroup.valid); this.sub.add(this.formGroup.valueChanges.subscribe(() => { this.duplicateAlert = this.referenceFieldService.getDuplicateFieldAlert( this.existingReferenceField?.referenceFieldId, this.formGroup.value ); this.setHelpers(); this.onFieldChange.emit(this.formGroup.value); })); this.sub.add(this.formGroup.statusChanges.subscribe(() => { this.onValidityChange.emit(this.formGroup.valid); })); this.afterInit = true; } ngOnChanges (changes: SimpleChanges) { if (this.afterInit && changes.tableImportNotAllowed) { const control = this.formGroup.get('tableAllowsImport'); if (this.tableImportNotAllowed) { if (control.value === true) { control.setValue(false); } } } } setHelpers () { this.setSupportsTableField(); this.setSupportsMaskAndEncrypt(); this.setShowSupportsMultiple(); } setTypeToDataPoint () { this.formGroup.get('type').setValue(ReferenceFieldsUI.ReferenceFieldTypes.DataPoint); } filterTypeOptions () { this.typeOptions = this.typeOptions.filter((opt) => { let typePasses = true; if (this.formGroup.value.formAudience === FormAudience.APPLICANT && this.audienceIsLocked) { typePasses = opt.value !== ReferenceFieldsUI.ReferenceFieldTypes.Aggregate; } if (this.addingFieldToForm) { typePasses = typePasses && opt.value !== ReferenceFieldsUI.ReferenceFieldTypes.DataPoint; } else if (this.forceIsTableField) { typePasses = typePasses && this.referenceFieldService.canBeTableField( opt.value, false, FormAudience.APPLICANT, false ); } return typePasses; }); } setIsExistingTableFieldOrDataPoint () { if (this.existingReferenceField?.referenceFieldId) { if (this.existingReferenceField.isTableField) { return this.fieldDetail?.hasTable; } else if ( this.existingReferenceField?.type === ReferenceFieldsUI.ReferenceFieldTypes.DataPoint ) { return true; } } return false; } getCanUpdateToSingleResponse () { if ( this.existingReferenceField?.referenceFieldId && this.existingReferenceField.formAudience === FormAudience.MANAGER ) { return this.referenceFieldService.getCanUpdateToSingleResponse( this.existingReferenceField.referenceFieldId ); } return true; } getCanUpdateCdtOnField () { const referenceFieldId = this.existingReferenceField?.referenceFieldId; if (this.isViewOnly || this.isMerge) { return false; } else if (referenceFieldId) { return this.referenceFieldService.canUpdateCdtOnField( referenceFieldId, this.existingReferenceField.type ); } return true; } setCustomDataTableOptions () { return this.customDataTableService.getDataTableOptions(); } onBlurOfName () { if ( !!this.formGroup.value.name && !this.formGroup.value.key ) { this.generateUniqueKey(); } } setFormattingTypeOptions () { this.formattingOptions = this.referenceFieldService.getFormattingTypeOptions(); } setParentReferenceField () { let parentRefField; if (this.existingReferenceField) { parentRefField = this.referenceFieldService.allReferenceFields.find((field) => { return field.referenceFieldId === this.existingReferenceField.parentReferenceFieldId; }); } else { parentRefField = null; } this.parentFieldExistsOnForm = (parentRefField?.formCount || 0) > 0; } setParentPicklistOptions () { if (!!this.existingReferenceField?.customDataTableGuid) { this.getParentPicklistOptions(this.existingReferenceField.customDataTableGuid); } } handleTypeChange () { this.resetFormattingIfNeeded(); this.resetParentRefField(); this.resetSupportsMulti(); this.ensureAudienceIsAccurate(); this.updateHelpText(); } supportsMultiChange (supportsMultiple: boolean) { if ( supportsMultiple && !this.isTableFieldDisabled && this.formGroup.value.isTableField ) { // If they click supports multi, turn off table field bc this is not allowed this.formGroup.get('isTableField').setValue(false); } } updateHelpText () { if (this.formGroup.value.type === ReferenceFieldsUI.ReferenceFieldTypes.Subset) { this.typeSelectHelp = this.subsetHelpText; } else if (this.formGroup.value.type === ReferenceFieldsUI.ReferenceFieldTypes.DataPoint) { this.typeSelectHelp = this.dataPointHelpText; } else { this.typeSelectHelp = null; } } resetFormattingIfNeeded () { if (this.formGroup.value.type !== ReferenceFieldsUI.ReferenceFieldTypes.TextField) { this.formGroup.get('formatType').setValue(null); } } ensureAudienceIsAccurate () { // Type "Table" is only for Applicant Audience if ( this.isTypeTable && this.formGroup.value.formAudience === FormAudience.MANAGER ) { this.formGroup.get('formAudience').setValue(FormAudience.APPLICANT); } } setSupportsTableField () { if (!this.forceIsTableField) { const formVal = this.formGroup.value; const canBeTableField = this.referenceFieldService.canBeTableField( formVal.type, formVal.supportsMultiple, formVal.formAudience, formVal.isTableField ); if (canBeTableField) { this.supportsTableField = true; } else { this.clearTableField(); } } } clearTableField () { this.supportsTableField = false; if (this.formGroup.value.isTableField) { this.formGroup.get('isTableField').setValue(false); } } setShowSupportsMultiple () { const show = this.referenceFieldService.showSupportsMultiCheckbox(this.formGroup.value.type); this.showSupportsMultiple = !this.forceIsTableField && show; } setSupportsMaskAndEncrypt () { const formVal = this.formGroup.value; const canMaskOrEncrypt = this.referenceFieldService.isCorrectTypeForMaskAndEncyrpt( formVal.type, formVal.isTableField ); if (canMaskOrEncrypt) { this.supportsEncryption = true; if (this.formGroup.value.formAudience === FormAudience.APPLICANT) { this.supportsMasking = true; } else { this.clearMasking(); } } else { this.clearEncryption(); this.clearMasking(); } } clearEncryption () { this.supportsEncryption = false; if (this.formGroup.value.isEncrypted) { this.formGroup.get('isEncrypted').setValue(false); } } clearMasking () { this.supportsMasking = false; if (this.formGroup.value.isMasked) { this.formGroup.get('isMasked').setValue(false); } } resetParentRefField () { if (this.formGroup) { this.formGroup.get('parentReferenceFieldId').setValue(null); } } resetSupportsMulti () { const canBeMulti = this.referenceFieldService.canBeMulti(this.formGroup.value.type); if (!canBeMulti && this.formGroup.value.supportsMultiple) { this.formGroup.get('supportsMultiple').setValue(false); } } getParentPicklistOptions (guid: string) { // reset formGroup value in case cdt is changed this.resetParentRefField(); // get new options with guid const refFieldId = this.existingReferenceField?.referenceFieldId; const options = this.referenceFieldService.getParentRefFieldOptionsFromDependentCDTGuid(guid, refFieldId); this.parentPicklistOptions = options; if (this.formGroup && options.length === 1) { this.formGroup.get('parentReferenceFieldId').setValue(options[0].value); } } dataTableHasParent (guid: string) { const picklist = this.customDataTableService.getCDTFromGuid(guid); return !!picklist.parentPicklistId; } generateUniqueKey () { const guessedKey = this.referenceFieldService.guessKey( this.formGroup.value.name, this.pendingKeyToBeCreated ? [this.pendingKeyToBeCreated] : [] ); this.formGroup.get('key').setValue(guessedKey); } forceSelectCustomDataTable () { // make sure if CustomDataTable is the type, that CDT options is selected return (group: AbstractControl) => { const forceSelect = this.referenceFieldService.doesTypeHaveOptions( group.value.type ); if (forceSelect) { if (!group.value.customDataTableGuid) { return { mustSelectDataTable: { customDataTableGuid: { i18nKey: 'GLOBAL:textMustSelectCustomDataTable', defaultValue: 'Must select the custom data table for this type' } } }; } return null; } return null; }; } forceSelectParentPicklistIfChild () { return (group: AbstractControl) => { if (group.value.customDataTableGuid) { const hasParent = this.dataTableHasParent(group.value.customDataTableGuid); const parentRequired = group.value.type === this.ReferenceFieldTypes.CustomDataTable && hasParent; if (parentRequired && !group.value.parentReferenceFieldId) { return { parentPicklistRequired: { parentReferenceFieldId: { i18nKey: 'FORMS:textParentPicklistRequired', defaultValue: 'Must select a parent picklist for dependent custom data table picklists' } } }; } return null; } return null; }; } forceSelectDataSetCollectionType () { return (group: TypeSafeFormGroup) => { if ( group.value.type === ReferenceFieldsUI.ReferenceFieldTypes.Subset && !group.value.subsetCollectionType ) { return { dataSetCollectionTypeRequired: { subsetCollectionType: { i18nKey: 'FORMS:textDataSetCollectionTypeIsRequired', defaultValue: 'Must select a collection type for data set fields' } } }; } return null; }; } forceSelectParentFormFieldIfAggregate () { return (group: TypeSafeFormGroup) => { if ( group.value.type === ReferenceFieldsUI.ReferenceFieldTypes.Aggregate && !group.value.parentReferenceFieldId ) { return { parentPicklistRequired: { parentReferenceFieldId: { i18nKey: 'FORMS:textParentFormFieldRequiredForAggregationFields', defaultValue: 'Must select a form field for aggregation fields' } } }; } return null; }; } ngOnDestroy () { this.sub.unsubscribe(); } }