import { Injectable } from '@angular/core'; import { ReferenceFieldAPI } from '@core/typings/api/reference-fields.typing'; import { ReferenceFieldsUI } from '@core/typings/ui/reference-fields.typing'; import { Form, FormData, FormDefinition, FormDefinitionComponent, FormDefinitionForUi, FormFieldPasteLocation, FormioAnswerValues, FormioChangesWithCompKey, FormioComponentType, FormTab } from '@features/configure-forms/form.typing'; import { EvaluationType, LogicFilterTypes } from '@features/logic-builder/logic-builder.typing'; import { FilterModalTypes, SimpleStringMap, TypeaheadSelectOption } from '@yourcause/common'; import { CurrencyRadioOptions } from '@yourcause/common/masking'; import { cloneDeep, get, set } from 'lodash'; export const REF_COMPONENT_TYPE_PREFIX = 'referenceFields-'; export const EMPLOYEE_SSO_TYPE_PREFIX = 'employeeInfo'; export const Total_Columns = 12; export const MAX_ROWS_TO_DISPLAY_PDF_TABLE = 5; export const MAX_COLUMNS_TO_DISPLAY_PDF_TABLE = 5; @Injectable({ providedIn: 'root' }) export class ComponentHelperService { /** * Flatten a form definition, removing all layout components, and returning a list of components * * @param formDef The form definition to flatten */ flattenFormDefinition (formDef: FormDefinition): FormDefinitionComponent[] { const comps: FormDefinitionComponent[] = []; this.eachComponent(formDef.components, c => { switch (c.type) { case 'columns': case 'well': case 'fieldset': case 'panel': case 'table': break; default: comps.push(c); break; } }, true); return comps; } /** * Call a function for each component in the list and their children. * * This function will be passed the component, the path to the component, and the key of the parent component (if present). * If the function returns `true`, the recursion will stop * * @param components List of components to iterate over * @param fn Function to call for each component found * @param includeAll Fire `fn` on layout components */ eachComponent ( components: FormDefinitionComponent[], fn: (component: FormDefinitionComponent, path: string, parentKey: string) => any, includeAll: boolean = false ) { this.doEachComponent(components, fn, includeAll, '', null); } private doEachComponent ( components: FormDefinitionComponent[], fn: (component: FormDefinitionComponent, path: string, parentKey: string) => any, includeAll: boolean, path: string, parent: FormDefinitionComponent|null ) { path = path || ''; components.forEach((component) => { if (!component) { return; } const hasColumns = component.columns && Array.isArray(component.columns); const hasRows = component.rows && Array.isArray(component.rows); const hasComps = component.components && Array.isArray(component.components); let noRecurse = false; const newPath = component.key ? (path ? (`${path}.${component.key}`) : component.key) : ''; if (includeAll || (!hasColumns && !hasRows && !hasComps)) { noRecurse = fn(component, newPath, parent?.key); } const subPath = () => { if ( component.key && !['panel', 'table', 'well', 'columns', 'fieldset', 'tabs', 'form'].includes(component.type) && ( ['datagrid', 'container', 'editgrid'].includes(component.type) ) ) { return newPath; } else if ( component.key && component.type === 'form' ) { return `${newPath}.data`; } return path; }; if (!noRecurse) { if (hasColumns) { component.columns.forEach((column) => { if (column.components) { return this.doEachComponent(column.components, fn, includeAll, subPath(), component); } }); } else if (hasRows) { component.rows.forEach((row) => { if (Array.isArray(row)) { row.forEach((column) => { if (column.components) { return this.doEachComponent(column.components, fn, includeAll, subPath(), component); } }); } }); } else if (hasComps) { this.doEachComponent(component.components, fn, includeAll, subPath(), component); } } }); } isLayoutComponent (type: string) { return [ 'content', 'columns', 'fieldset', 'panel', 'table', 'well' ].includes(type); } /** * * @param type: component type * @returns if the component type is a standard component */ isStandardComponent (type: string) { return [ 'amountRequested', 'inKindItems', 'careOf', 'designation', 'decision', 'reviewerRecommendedFundingAmount', 'specialHandling' ].includes(type); } /** * * @param comp: form component * @returns if component is visible */ isCompVisible ( comp: FormDefinitionComponent ): boolean { return !comp.isHidden && !comp.hiddenFromParent; } /** * * @param component: the component * @returns the components inside of it */ getNestedComponents (component: FormDefinitionComponent): FormDefinitionComponent[] { const formDefinition: FormDefinitionForUi[] = [{ tabName: '', components: [ component ], uniqueId: '', index: 0, logic: null }]; return this.getAllComponents(formDefinition); } /** * Return an array of all components in a list of form definitions * * @param formDefs List of form definitions * @param includeAll Return layout components * @returns A list of components */ getAllComponents ( formDefs: FormDefinitionForUi[], includeAll = false ): FormDefinitionComponent[] { return formDefs.reduce((acc, formDef) => { const temp: FormDefinitionComponent[] = []; this.eachComponent(formDef.components, comp => { temp.push(comp); }, includeAll); return [ ...acc, ...temp ]; }, []); } /** * * @param visibleTabs: visible tabs * @returns the required components */ getRequiredComponents ( visibleTabs: (FormDefinitionForUi | FormTab)[] ): FormDefinitionComponent[] { const requiredComps: FormDefinitionComponent[] = []; visibleTabs.forEach((tab) => { this.eachComponent((tab as FormDefinitionForUi).components, (component) => { const isVisible = this.isCompVisible(component); if (isVisible && component.validate?.required) { requiredComps.push(component); } }); }); return requiredComps; } /** * * @param type: comp type * @returns adapted type */ getAdaptedTypeFromComponentType ( type: string ): FormioComponentType { if (this.isReferenceFieldComp(type)) { return 'referenceField'; } else if (type.includes(EMPLOYEE_SSO_TYPE_PREFIX)) { return 'employeeSSO'; } else if (type === 'htmlelement') { type = 'content'; } return type as FormioComponentType; } /** * * @param component: the component * @param skipVisibility: are we skipping visibility? * @returns if the component is applicable for pdf */ isComponentApplicableForPdf ( component: FormDefinitionComponent, skipVisibility: boolean ) { const invalidCompTypes = [ 'tabs', 'form', 'button', 'htmlelement' ]; const correctType = !invalidCompTypes.includes(component.type.toLowerCase()); if ( correctType && (skipVisibility || this.isCompVisible(component)) ) { return true; } return false; } /** * @param existingColumns: existing columns on the component * @returns a blank column component */ getBlankColumnComponent ( existingColumns?: FormDefinitionComponent[] ): FormDefinitionComponent { let width = 6; if (existingColumns) { const totalWidth = this.getTotalColumnWidth(existingColumns); width = Total_Columns - totalWidth; } return { components: [], key: 'column', label: 'Column', offset: 0, pull: 0, push: 0, suffix: '', type: 'column', width }; } /** * * @param columns: columns for component * @returns can columns be added to that component */ canColumnCompAddColumns (columns: FormDefinitionComponent[]) { const totalWidth = this.getTotalColumnWidth(columns); return totalWidth < Total_Columns; } /** * * @param columns: columns for component * @returns the total column width */ getTotalColumnWidth (columns: FormDefinitionComponent[]) { return columns.reduce((acc, column) => { return acc + +column.width; }, 0); } /** * Removes a component from a form definition * * @param componentToRemove The component intended to be removed * @param formDefinition The form definition the component exists on * @param componentToReplaceWith Optionally, replace the component with this arg */ removeOrReplaceFormComponent ( componentToRemove: FormDefinitionComponent, formDefinition: FormDefinitionForUi, componentToReplaceWith?: FormDefinitionComponent ) { const parent = this.getParentComponent(componentToRemove, formDefinition); if (parent) { if ('columns' in parent && parent.columns && Array.isArray(parent.columns)) { parent.columns = parent.columns.map(column => { this.updateParentComponents(column, componentToRemove, componentToReplaceWith); return column; }); } else if ('rows' in parent && parent.rows && Array.isArray(parent.rows)) { parent.rows = parent.rows.map(row => { row.forEach(column => { this.updateParentComponents(column, componentToRemove, componentToReplaceWith); }); return row; }); } else { this.updateParentComponents(parent, componentToRemove, componentToReplaceWith); } } } /** * * @param parent: parent component * @param componentToRemove: component to remove * @param componentToReplace: component to replace */ private updateParentComponents ( parent: { components?: FormDefinitionComponent[]; }, componentToRemove: FormDefinitionComponent, componentToReplace?: FormDefinitionComponent ) { const index = parent.components.findIndex((comp) => { return comp.key === componentToRemove.key; }); if (index !== -1) { parent.components = [ ...parent.components.slice(0, index), ...(componentToReplace ? [componentToReplace] : []), ...parent.components.slice(index + 1) ]; } } /** * Inserts a form component into a form definition. * * If a `relativeComponent` is not passed, the `componentToInsert` will be appended to the end * * otherwise put `componentToInsert` after `relativeComponent` * * @param componentToInsert The component to insert * @param formDefinition The form definition the component is inserted into * @param relativeComponent Component location to insert relative to * @param pasteLocation Where to insert the form component */ insertFormComponent ( componentToInsert: FormDefinitionComponent, formDefinition: FormDefinitionForUi, relativeComponent?: FormDefinitionComponent, pasteLocation?: FormFieldPasteLocation ) { if (relativeComponent) { const parent = this.getParentComponent(relativeComponent, formDefinition); if (parent) { if ('columns' in parent && parent.columns && Array.isArray(parent.columns)) { parent.columns = parent.columns.map((column) => { column.components = column.components.reduce((acc, comp) => { return this.reduceComponentIntoArray( comp, relativeComponent, componentToInsert, acc, pasteLocation ); }, []); return column; }); } else if ('rows' in parent && parent.rows && Array.isArray(parent.rows)) { parent.rows = parent.rows.map((rows) => { return rows.map((row) => { row.components = row.components.reduce((acc, comp) => { return this.reduceComponentIntoArray( comp, relativeComponent, componentToInsert, acc, pasteLocation ); }, []); return row; }); }); } else if (parent.components) { parent.components = parent.components.reduce((acc, comp) => { return this.reduceComponentIntoArray( comp, relativeComponent, componentToInsert, acc, pasteLocation ); }, []); } } } else { formDefinition.components.push(componentToInsert); } } /** * * @param thisComp: the component we are evaluating * @param relativeComp: the relative component we are looking for * @param compToInsert: the component to insert * @param acc: the accrued array of components * @returns the reduced array */ reduceComponentIntoArray ( thisComp: FormDefinitionComponent, relativeComp: FormDefinitionComponent, compToInsert: FormDefinitionComponent, acc: FormDefinitionComponent[], pasteLocation: FormFieldPasteLocation ) { if (thisComp.key === relativeComp.key) { let accumulated; switch (pasteLocation) { default: case FormFieldPasteLocation.BELOW: accumulated = [ ...acc, thisComp, compToInsert ]; break; case FormFieldPasteLocation.ABOVE: accumulated = [ ...acc, compToInsert, thisComp ]; break; case FormFieldPasteLocation.INSIDE_CONTAINER: accumulated = [ ...acc, { ...thisComp, components: [ compToInsert, ...thisComp.components ] } ]; break; } return accumulated; } else { return [ ...acc, thisComp ]; } } /** * Return the parent component definition of the child, if the child is a root component, returns the form definition * * @param childComponent The child component used for searching * @param formDef The form definition the child belongs to * @returns The parent component or form */ getParentComponent ( childComponent: FormDefinitionComponent, formDef: FormDefinitionForUi ) { let parentKey: string; let foundChild = false; this.eachComponent(formDef.components, (component, _, _parentKey) => { if (component.key === childComponent.key) { foundChild = true; parentKey = _parentKey; return true; } return false; }, true); if (foundChild) { return parentKey ? this.findComponentByKey([formDef], parentKey) : formDef; } return null; } /** * Search a list of form definitions for a component by its key * * @param formDefs List of form definitions * @param componentKey The key of the component to be found * @returns A component (if present) */ findComponentByKey ( formDefs: FormDefinitionForUi[], componentKey: string ) { for (const formDef of formDefs) { let foundComponent: FormDefinitionComponent; this.eachComponent(formDef.components, comp => { if (comp.key === componentKey) { foundComponent = comp; return true; } return false; }, true); if (foundComponent) { return foundComponent; } } return null; } /** * * @param compType: component type * @param formDefinition: form definition * @returns if the comp type already exists on the tab */ checkIfCompTypeExistsOnTab ( compType: string, formDefinition: FormDefinitionForUi ) { let exists = false; this.eachComponent(formDefinition.components, (comp) => { if (comp.type === compType) { exists = true; } }); return exists; } /** * * @param formDefinition: the form definitino * @returns if the form has amount requested on it */ formHasAmountRequested (formDefinition: FormDefinitionForUi[]) { let hasAmountRequested = false; formDefinition.forEach((tab) => { this.eachComponent(tab.components, (comp) => { if (comp.type === 'amountRequested') { hasAmountRequested = true; } }); }); return hasAmountRequested; } /** * * @param compType: Component type * @returns if the component is a reference field type */ isReferenceFieldComp (compType: string) { return compType.startsWith(REF_COMPONENT_TYPE_PREFIX); } /** * Gets the reference field key from the component type * * @param type: component type * @returns the ref key */ getRefFieldKeyFromCompType (type: string) { if (this.isReferenceFieldComp(type)) { return type.split('-')[1]; } return null; } /** * * @param component: the component * @returns if it's disabled */ isCompDisabled ( component: FormDefinitionComponent ): boolean { if (component) { if (component.disabled) { return true; } else if (component.allowCalculateOverride) { return false; } return (component.conditionalValue?.length > 0) || !!component.formula?.step || !!component.calculateValue; } return false; } /** * * @param tableColumn: the table column * @returns the component from the table column */ getComponentFromTableColumn ( tableColumn: ReferenceFieldsUI.TableFieldForUi ): FormDefinitionComponent { const field = tableColumn.referenceField; const type = `referenceFields-${field.key}`; const componentReturn: FormDefinitionComponent = { components: [], key: field.key, type, label: tableColumn.label, validate: { required: tableColumn.isRequired }, placeholder: '', useCustomCurrency: CurrencyRadioOptions.USE_ONE_CURRENCY }; return componentReturn; } /** * * @param storedCurrency: currency stored on application * @param forceDefaultCurrency: are we forcing them to fill it out in default currency of client? * @param componentCurrencySetting: the setting stored on the component for which currency to use * @param currencyOptions: currency options based on the above setting * @param defaultCurrency: client default currency * @param lastSelectedCurrency: the last currency used by user * @returns the currency to set in the form group */ getCurrencyForFormFieldControl ( storedCurrency: string, useCustomCurrencySetting: CurrencyRadioOptions, customCurrencySetting: string, currencyOptions: TypeaheadSelectOption[], defaultCurrency: string, lastSelectedCurrency: string ) { const componentCurrencySetting = useCustomCurrencySetting === CurrencyRadioOptions.USE_ANY_CURRENCY ? null : customCurrencySetting; if (storedCurrency) { return storedCurrency; } else if (componentCurrencySetting) { return componentCurrencySetting; } else { // If no currency on app, we attempt to default to the applicant's last selected currency if (lastSelectedCurrency) { const mappedOptions = currencyOptions.map((opt) => opt.value); const exists = mappedOptions.includes(lastSelectedCurrency); if (exists) { return lastSelectedCurrency; } } } return defaultCurrency; } /** * * @param formDefinition: the form definition * @param translations: the translations */ replaceStandardText ( formDefinition: FormDefinitionForUi[], translations: Record ) { formDefinition.forEach((tab) => { tab.tabName = translations[tab.tabName] || tab.tabName; this.eachComponent(tab.components, (component) => { component.label = translations[component.label] || component.label; component.description = translations[component.description] || component.description; }); }); } /** * * @param formDefinition: the form definition * @param richTextTranslations: the rich text translations */ replaceRichText ( formDefinition: FormDefinitionForUi[], richTextTranslations: Record ) { formDefinition.forEach((tab) => { this.eachComponent(tab.components, (component) => { if (component.type === 'content') { Object.keys(richTextTranslations).forEach((key) => { if ((component.html || '').trim() === (key || '').trim()) { component.html = richTextTranslations[key]; } }); } }); }); } applyTranslationsToComponents ( formDefinition: FormDefinitionForUi[], translations: SimpleStringMap, richTextTranslations: SimpleStringMap ) { this.replaceStandardText(formDefinition, translations); this.replaceRichText(formDefinition, richTextTranslations); } /** * Some components have bad data here where: * The component is set to conditionally show, but no conditions exist. * This technically means that it should always show, so update the configuration * * @param comp: the component */ adaptConditionalLogic (comp: FormDefinitionComponent) { if (comp.conditionalLogic) { if ( comp.conditionalLogic.evaluationType === EvaluationType.ConditionallyTrue && comp.conditionalLogic.conditions?.length === 0 ) { comp.conditionalLogic.evaluationType = EvaluationType.AlwaysTrue; } } } /** * Adapt for bad data on conditional value (bug 2025300) * * @param comp: the component * @param isCurrencyField: is currency? */ adaptConditionalValue ( comp: FormDefinitionComponent, isCurrencyField: boolean ) { if (comp.conditionalValue) { comp.conditionalValue.forEach((rule) => { if (!rule.conditions) { rule.conditions = []; } // We used to store currency results as an object with amount and currency // This model was updated, so we need to make sure it's typed correctly if (isCurrencyField) { const result = rule.result as any; if ( result && result instanceof Object && 'amount' in result && 'currency' in result ) { rule.result = { amountInDefaultCurrency: result.amount, amountEquivalent: result.amount, amountForControl: result.amount, currency: result.currency }; } } }); } } /** * This is because with form.io we stored a lot of configs as strings * * @param comp: Component that may need configuration options parsed */ parseConfigurationOptions (comp: FormDefinitionComponent) { if (comp.type === 'reportField') { const options = comp.reportFieldDataOptions; if (options && typeof options === 'string') { try { comp.reportFieldDataOptions = JSON.parse(options); } catch (e) { } } } else if (comp.type === 'inKindItems') { const items = comp.items; if (items && typeof items === 'string') { try { comp.items = JSON.parse(items); } catch (e) { } } } else { if (comp.apiConfig && typeof comp.apiConfig === 'string') { try { comp.apiConfig = JSON.parse(comp.apiConfig); } catch (e) { } } } } /** * * @param customValidation the custom validation string stored in the form definition * * We had a bug (1875374) where in the form builder, we were running validation and overwriting the custom validation attribute with the error message. * This caused custom validation to be populated on some fields with error messages, resulting in the error always displaying. This funciton is to adapt and clear that invalid validation that was set */ adaptInitialCustomValidation (customValidation: string) { let adaptedCustomValidation; if (customValidation) { const pattern = /^return ('|").*('|")/; const remove = pattern.test(customValidation) || customValidation === 'return null;'; adaptedCustomValidation = remove ? null : customValidation; } return adaptedCustomValidation; } /** * * @param component: form component */ clearConditional (component: FormDefinitionComponent) { // Store old conditional in case we ever need to access it component.oldConditonal = component.conditional; component.conditional = { show: '', when: '', json: '', eq: '' }; } /** * * @param value: value * @param filterType: filter type * @param compType: component type * @returns value and comparison */ getValueAndCondition ( value: string, filterType: LogicFilterTypes, compType: string ) { let adaptedValue: string|boolean = value; // For checkboxes, we convert to booleans if (value === 'true') { adaptedValue = true; } else if (value === 'false') { adaptedValue = false; } switch (filterType) { default: return { value: adaptedValue, comparison: FilterModalTypes.equals }; case 'multi-list': const isNumber = !isNaN(value as number); if (isNumber && compType === 'decision') { // Decision needs to use numbers instead of strings return { value: [+value], comparison: FilterModalTypes.equals }; } else { return { value: [value], comparison: FilterModalTypes.equals }; } case 'multiValueList': case 'multiValueText': return { value: [value], comparison: FilterModalTypes.multiValueEquals }; } } /** * * @param formDetail: the form detail * @param pageUniqueId: unique page id * @param existingComps: existing comps * @returns duplicated keys & duplicate fields */ getDuplicateKeysAndFields ( formDetail: Form, pageUniqueId: string, existingComps: FormDefinitionComponent[] ) { const tabComps = this.getComponentsFromTab(formDetail, pageUniqueId); const duplicateKeys: FormDefinitionComponent[] = []; const duplicateFields: FormDefinitionComponent[] = []; tabComps.forEach((compToAdd) => { existingComps.forEach((existingComp) => { if (compToAdd.key === existingComp.key) { duplicateKeys.push(existingComp); } if (compToAdd.type === existingComp.type) { duplicateFields.push(existingComp); } }); }); return { duplicateKeys, duplicateFields }; } /** * * @param formDetail: the form detail * @param pageUniqueId: unique page id * @returns the components from the tab */ getComponentsFromTab ( formDetail: Form, pageUniqueId: string ) { const comps: FormDefinitionComponent[] = []; formDetail.formDefinition.filter((def) => { return def.uniqueId === pageUniqueId; }).forEach((def) => { this.eachComponent(def.components, (comp) => { if ( comp.type !== 'content'&& comp.type !== 'button' ) { comps.push(comp); } }); }); return comps; } /** * Determines if a value was manually overwritten by the user * * @param isFirstRun Is this the first time the logic has been run for the comp * @param currentRunResult The current result of the logic * @param previousRunResult The previous result of the logic * @param formResponse The current value for the component * @param componentType The component `type` * * @returns The response that should be set */ determineCorrectCalculatedValueResult ( isFirstRun: boolean, currentRunResult: FormioAnswerValues, previousRunResult: FormioAnswerValues, formResponse: FormioAnswerValues, componentType: string, allowCalculateOverride: boolean ) { if (allowCalculateOverride) { let currentValue: FormioAnswerValues; const hasEmptyResponse = formResponse === undefined || formResponse === null || formResponse === '' || ( (componentType === 'amountRequested' || componentType === 'reviewerRecommendedFundingAmount') && +formResponse === 0 ); const previousResultAndResponseAreNaN = ( previousRunResult === 'NaN' || ( typeof previousRunResult === 'number' && isNaN(previousRunResult) ) ) && ( formResponse === 'NaN' || ( typeof formResponse === 'number' && isNaN(formResponse) ) ); const firstRunForComponentWithValue = isFirstRun && !hasEmptyResponse; const componentChangedManually = !isFirstRun && !previousResultAndResponseAreNaN && // eslint-disable-next-line eqeqeq (previousRunResult != formResponse); const needToUseRunResult = !firstRunForComponentWithValue && !componentChangedManually; if (needToUseRunResult) { currentValue = currentRunResult; } else { currentValue = formResponse as FormioAnswerValues; } return { currentValue, usedRunResult: needToUseRunResult }; } else { return { currentValue: currentRunResult, usedRunResult: true }; } } /** * * @param component: form component * @returns component data */ getComponentInputData (component: FormDefinitionComponent) { if (component.type !== 'button') { const inputData = { key: component.key, label: component.label, type: component.type, values: [] as any[] }; return inputData; } return null; } /** * * @param definition: form definition */ trimFromDefinitionData (definition: FormDefinitionForUi[]) { const keys = [ 'label', 'placeholder', 'description', 'errorLabel', 'tooltip', 'validate.customMessage' ]; definition.forEach((tab) => { this.eachComponent(tab.components, (component) => { keys.forEach((key) => { set(component, key, get(component, key, '').trim()); }); }, true); }); } /** * * @param pastedComponent: the pasted component * @param formDefs: form definition * @returns cloned component */ prepComponentForPaste ( pastedComponent: FormDefinitionComponent, formDefs: FormDefinitionForUi[] ) { const comps = this.getAllComponents(formDefs, true); const cloned = cloneDeep(pastedComponent); this.eachComponent([cloned], (comp) => { comp.key = this.guessKey(comp, comps); }, true); return cloned; } /** * * @param formDefinition: the form definition * @returns if the comp has been updated / changed */ guessKeys (formDefinition: FormDefinitionForUi[]) { let hasChanged = false; const allComps = this.getAllComponents(formDefinition, true); allComps.forEach((component) => { const isLayout = this.isLayoutComponent(component.type); if (isLayout) { const newKey = this.guessKey(component, allComps); if (newKey !== component.key) { component.key = newKey; hasChanged = true; } } }); return hasChanged; } /** * * @param component: the component * @param compArray: the components array * @param count: the count (only used recursively) * @returns the new key */ guessKey ( component: FormDefinitionComponent, compArray: FormDefinitionComponent[], count = 0 ): string { const guessedKey = component.key + (count || ''); const nextComponentExists = compArray.some(comp => { return comp !== component && (comp.key === guessedKey); }); if (nextComponentExists) { return this.guessKey(component, compArray, count ? count + 1 : 2); } else { return guessedKey; } } /** * * @param visibleColumns: visible table columns * @returns if any columns have summarize data (aggregate) */ getIsTableTotaled ( visibleColumns: ReferenceFieldsUI.TableFieldForUi[] ): boolean { return visibleColumns.some((column) => { return column.summarizeData; }); } /** * * @param formDefinition: the form definition * @returns the content keys */ getContentKeysFromFormDef (formDefinition: FormDefinitionForUi[]) { const contentKeysToRemove: string[] = []; formDefinition.forEach((tab) => { this.eachComponent(tab.components, (comp) => { if (comp.type === 'content') { contentKeysToRemove.push(comp.key); } }); }); return contentKeysToRemove; } /** * * @param formData: form data * @param formDefinition: the form definition * @returns the form data with content keys removed */ removeContentFromFormData ( formData: FormData, formDefinition: FormDefinitionForUi[] ) { const keysToRemove = this.getContentKeysFromFormDef(formDefinition); keysToRemove.forEach((key) => { delete formData[key]; }); return formData; } /** * * @param formDefinition: the form definition * @returns reference field comps on the form */ getRefCompsOnForm (formDefinition: FormDefinitionForUi[]) { const refCompTypes: string[] = []; formDefinition.forEach((tab) => { this.eachComponent(tab.components, (comp) => { if (this.isReferenceFieldComp(comp.type)) { refCompTypes.push(comp.type); } }); }); return refCompTypes; } /** * * @param components: the components * @returns the reference field components */ extractReferenceFieldComponents (components: FormDefinitionComponent[]) { const refComponents: FormDefinitionComponent[] = []; this.eachComponent(components, (comp) => { if (this.isReferenceFieldComp(comp.type)) { refComponents.push(comp); } }); return refComponents; } /** * * @param component: the component * @param formDefinition: the form definition * @returns reference field components to copy */ getRefFieldComponentsToCopy ( component: FormDefinitionComponent, formDefinition: FormDefinitionForUi[] = [] ) { let components: FormDefinitionComponent[] = []; const refCompTypes = this.getRefCompsOnForm(formDefinition); const nestedComponents = this.getNestedComponents( component ); if (nestedComponents) { components = this.extractReferenceFieldComponents( nestedComponents ); } // Only include components that still live on form components = components.filter((comp) => { return refCompTypes.includes(comp.type); }); return components; } /** * * @param rows field group rows * @returns the total of the fields */ getFieldGroupTotal (rows: ReferenceFieldsUI.TableResponseRowForUi[]) { let sumOfFields = 0; // Field groups only have 1 row if (rows && rows[0]) { sumOfFields = rows[0].columns.reduce((acc, col) => { return acc + +((col.value as number) || 0); }, 0); sumOfFields = +(sumOfFields.toFixed(2)); } return sumOfFields; } /** * * @param component: the component * @param skipVisibility: are we skipping visibility? * @param applicableComponents: applicable components to potentially push */ updateApplicableComponentsArray ( component: FormDefinitionComponent, skipVisibility = false, applicableComponents: FormDefinitionComponent[] ) { const passes = this.isComponentApplicableForPdf( component, skipVisibility ); if (passes) { applicableComponents.push(component); } } /** * Creates a map of the eligibility formData that needs to get added to the reference field responses map * * @param formData: Form Data from old form.io logic * @param responses: Reference field responses */ findFormDataToAddToResponses ( formData: FormData, formDefinition: FormDefinitionForUi[] ) { const map: Record = {}; formData = this.removeContentFromFormData(formData, formDefinition); Object.keys(formData).forEach((key) => { const comp = this.findComponentByKey(formDefinition, key); if (comp && this.isReferenceFieldComp(comp.type)) { const refKey = this.getRefFieldKeyFromCompType(comp.type); map[refKey] = formData[key]; } }); return map; } /** * Gets whether we should show the table on PDF based on record count * * @param referenceFields: ref responses * @param referenceField: the reference field * @param numberOfCols: number of columns in the table or subset * @returns whether to show table on pdf and total table rows */ getShowTableOnPdfAndRecordCount ( referenceFields: ReferenceFieldsUI.RefResponseMap, field: ReferenceFieldAPI.ReferenceFieldDisplayModel, numberOfCols: number ): { showTableOnPdf: boolean; totalTableRows: number; } { if (field?.type === ReferenceFieldsUI.ReferenceFieldTypes.Table) { const rows = referenceFields[field.key] as ReferenceFieldsUI.TableResponseRowForUi[]; return { showTableOnPdf: numberOfCols <= MAX_COLUMNS_TO_DISPLAY_PDF_TABLE && rows.length <= MAX_ROWS_TO_DISPLAY_PDF_TABLE, totalTableRows: rows.length }; } else if (field?.type === ReferenceFieldsUI.ReferenceFieldTypes.Subset) { // we will render the columns as rows in the template, so we check against max rows return { showTableOnPdf: numberOfCols <= MAX_ROWS_TO_DISPLAY_PDF_TABLE, totalTableRows: numberOfCols }; } return { showTableOnPdf: false, totalTableRows: 0 }; } /** * We only support pasting into certain types of layout components, e.g. wells, columns * * @param type the type of the form component we are attempting to paste into * @returns boolean for whether the component is a container that can be pasted into */ allowPasteIntoContainer (type: string) { const isLayoutComponent = this.isLayoutComponent(type); return isLayoutComponent && !['columns', 'content'].includes(type); } /** * Adapts and returns form changes * * @param component: the component * @param value: the value of the component * @param updateFormGroup: does this change require form group to be updated? * @returns adapted change info */ adaptToFormChanges ( component: FormDefinitionComponent, value: FormioAnswerValues, updateFormGroup: boolean ): FormioChangesWithCompKey { let key = component.key; const isReferenceField = this.isReferenceFieldComp(component.type); if (isReferenceField) { key = this.getRefFieldKeyFromCompType(component.type); } return { key, type: component.type, isReferenceField, value, componentKey: component.key, updateFormGroup }; } }