From e91820748cc2e60e2ee1e1245b4e97c927e92969 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 14 Feb 2024 14:40:17 +0000 Subject: [PATCH 1/4] fix: handle undefined values in answer lists --- src/app/shared/utils/utils.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/app/shared/utils/utils.ts b/src/app/shared/utils/utils.ts index 04ae22ae5e..4cbec07e26 100644 --- a/src/app/shared/utils/utils.ts +++ b/src/app/shared/utils/utils.ts @@ -437,7 +437,15 @@ export function parseAnswerList(answerList: any) { return parseAnswerListItem(item); } ); - return answerListItems; + // Remove any items from the list which only have a value for "name", + // e.g. "image" and "text" are undefined because the list has been generated within a template + const filteredAnswerListItems = answerListItems.filter((item: IAnswerListItem) => { + for (let [key, value] of Object.entries(item)) { + if (key !== "name" && value !== undefined) return true; + } + return false; + }); + return filteredAnswerListItems; } /** @@ -449,8 +457,9 @@ function parseAnswerListItem(item: any) { if (typeof item === "string") { const stringProperties = item.split("|"); stringProperties.forEach((s) => { - const [field, value] = s.split(":").map((v) => v.trim()); + let [field, value] = s.split(":").map((v) => v.trim()); if (field && value) { + if (["undefined", "NaN", "null", '""'].includes(value)) value = undefined; itemObj[field] = value; } }); From c7732ba85b3dd36d4acad86f683d80386ac77572 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 20 Feb 2024 11:40:31 +0000 Subject: [PATCH 2/4] fix: only convert undefined to undefined in answer list parsing --- src/app/shared/utils/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/utils/utils.ts b/src/app/shared/utils/utils.ts index 4cbec07e26..c8beef4481 100644 --- a/src/app/shared/utils/utils.ts +++ b/src/app/shared/utils/utils.ts @@ -459,7 +459,7 @@ function parseAnswerListItem(item: any) { stringProperties.forEach((s) => { let [field, value] = s.split(":").map((v) => v.trim()); if (field && value) { - if (["undefined", "NaN", "null", '""'].includes(value)) value = undefined; + if (value === "undefined") value = undefined; itemObj[field] = value; } }); From 082196b97bc6979b27b5dc7edda44e80ccb157f0 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 20 Feb 2024 11:47:02 +0000 Subject: [PATCH 3/4] refactor: filteredAnswerListItems logic --- src/app/shared/utils/utils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/shared/utils/utils.ts b/src/app/shared/utils/utils.ts index c8beef4481..0596e6a8f2 100644 --- a/src/app/shared/utils/utils.ts +++ b/src/app/shared/utils/utils.ts @@ -440,10 +440,10 @@ export function parseAnswerList(answerList: any) { // Remove any items from the list which only have a value for "name", // e.g. "image" and "text" are undefined because the list has been generated within a template const filteredAnswerListItems = answerListItems.filter((item: IAnswerListItem) => { - for (let [key, value] of Object.entries(item)) { - if (key !== "name" && value !== undefined) return true; - } - return false; + const hadItemData = Object.entries(item).find( + ([key, value]) => key !== "name" && value !== undefined + ); + return hadItemData ? true : false; }); return filteredAnswerListItems; } From 265c30cc79805d2c434c0986b28cdb3bcd65ff83 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 21 Feb 2024 15:53:30 +0000 Subject: [PATCH 4/4] refactor: incorporate parseAnswerList into new getAnserListParamFromTempla teRow util function --- .../combo-box-modal.component.ts | 8 +- .../combo-box/combo-box.component.ts | 14 ++- .../radio-button-grid.component.ts | 5 +- .../radio-group/radio-group.component.ts | 15 ++- src/app/shared/utils/utils.ts | 119 ++++++++++-------- 5 files changed, 84 insertions(+), 77 deletions(-) diff --git a/src/app/shared/components/template/components/combo-box/combo-box-modal/combo-box-modal.component.ts b/src/app/shared/components/template/components/combo-box/combo-box-modal/combo-box-modal.component.ts index 3fb9c64781..ba1158cb2f 100644 --- a/src/app/shared/components/template/components/combo-box/combo-box-modal/combo-box-modal.component.ts +++ b/src/app/shared/components/template/components/combo-box/combo-box-modal/combo-box-modal.component.ts @@ -1,16 +1,14 @@ import { Component, Input, OnInit } from "@angular/core"; import { FlowTypes } from "src/app/shared/model"; import { + getAnswerListParamFromTemplateRow, getBooleanParamFromTemplateRow, getNumberParamFromTemplateRow, - getParamFromTemplateRow, getStringParamFromTemplateRow, IAnswerListItem, - parseAnswerList, } from "src/app/shared/utils"; import { FormBuilder, FormControl, FormGroup } from "@angular/forms"; import { ModalController } from "@ionic/angular"; -import { objectToArray } from "../../../utils"; @Component({ selector: "combo-box-modal", @@ -39,9 +37,7 @@ export class ComboBoxModalComponent implements OnInit { } getParams() { - this.valuesFromListAnswers = parseAnswerList( - getParamFromTemplateRow(this.row, "answer_list", null) - ); + this.valuesFromListAnswers = getAnswerListParamFromTemplateRow(this.row, "answer_list", null); this.textTitle = getStringParamFromTemplateRow(this.row, "text", null); this.inputAllowed = getBooleanParamFromTemplateRow(this.row, "input_allowed", false); this.inputPosition = diff --git a/src/app/shared/components/template/components/combo-box/combo-box.component.ts b/src/app/shared/components/template/components/combo-box/combo-box.component.ts index 5e309e8bfe..1054119771 100644 --- a/src/app/shared/components/template/components/combo-box/combo-box.component.ts +++ b/src/app/shared/components/template/components/combo-box/combo-box.component.ts @@ -3,10 +3,10 @@ import { FlowTypes } from "../../../../model"; import { ModalController } from "@ionic/angular"; import { ComboBoxModalComponent } from "./combo-box-modal/combo-box-modal.component"; import { + IAnswerListItem, + getAnswerListParamFromTemplateRow, getBooleanParamFromTemplateRow, - getParamFromTemplateRow, getStringParamFromTemplateRow, - parseAnswerList, } from "src/app/shared/utils"; import { TemplateBaseComponent } from "../base"; import { ITemplateRowProps } from "../../models"; @@ -29,25 +29,26 @@ export class TmplComboBoxComponent text = ""; customAnswerSelected: boolean = false; customAnswerText: string; + answerList: IAnswerListItem[]; private componentDestroyed$ = new ReplaySubject(1); + constructor(private modalController: ModalController, private templateService: TemplateService) { super(); } ngOnInit(): void { this.getParams(); - const answerList = parseAnswerList(getParamFromTemplateRow(this._row, "answer_list", [])); this.customAnswerSelected = - answerList.length > 0 && this._row.value - ? !answerList.find((x) => x.name === this._row.value) + this.answerList.length > 0 && this._row.value + ? !this.answerList.find((x) => x.name === this._row.value) : false; this.text = ""; if (this._row.value) { this.text = this.customAnswerSelected ? this.customAnswerText - : answerList.find((answerListItem) => answerListItem.name === this._row.value)?.text; + : this.answerList.find((answerListItem) => answerListItem.name === this._row.value)?.text; } } @@ -59,6 +60,7 @@ export class TmplComboBoxComponent false ); this.style = getStringParamFromTemplateRow(this._row, "style", ""); + this.answerList = getAnswerListParamFromTemplateRow(this._row, "answer_list", []); } async openModal() { diff --git a/src/app/shared/components/template/components/radio-button-grid/radio-button-grid.component.ts b/src/app/shared/components/template/components/radio-button-grid/radio-button-grid.component.ts index 2d4443be99..1ac5ff418c 100644 --- a/src/app/shared/components/template/components/radio-button-grid/radio-button-grid.component.ts +++ b/src/app/shared/components/template/components/radio-button-grid/radio-button-grid.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; import { TemplateBaseComponent } from "../base"; import { FlowTypes, ITemplateRowProps } from "../../models"; -import { getParamFromTemplateRow, IAnswerListItem, parseAnswerList } from "src/app/shared/utils"; +import { getAnswerListParamFromTemplateRow, IAnswerListItem } from "src/app/shared/utils"; interface IRadioButtonGridParams { /** List of options presented as radio items */ @@ -65,8 +65,7 @@ export class TmplRadioButtonGridComponent private setParams() { this.parameter_list = this._row.parameter_list || ({} as any); - const answerList = getParamFromTemplateRow(this._row, "answer_list", []); - this.radioItems = parseAnswerList(answerList); + this.radioItems = getAnswerListParamFromTemplateRow(this._row, "answer_list", []); this.gridStyle = this.generateGridStyle(); } diff --git a/src/app/shared/components/template/components/radio-group/radio-group.component.ts b/src/app/shared/components/template/components/radio-group/radio-group.component.ts index 9f973deecc..07f9ec506c 100644 --- a/src/app/shared/components/template/components/radio-group/radio-group.component.ts +++ b/src/app/shared/components/template/components/radio-group/radio-group.component.ts @@ -14,8 +14,8 @@ import { getNumberParamFromTemplateRow, getParamFromTemplateRow, getStringParamFromTemplateRow, - parseAnswerList, IAnswerListItem, + getAnswerListParamFromTemplateRow, } from "../../../../utils"; import { ReplaySubject } from "rxjs"; @@ -38,7 +38,7 @@ export class TmplRadioGroupComponent flexWidth: string; // Parameters - answer_list: string[]; + answerList: IAnswerListItem[]; options_per_row: number; radioButtonType: string | null; style: string; @@ -56,8 +56,8 @@ export class TmplRadioGroupComponent this.windowWidth = window.innerWidth; // convert string answer lists to formatted objects - this.answer_list = getParamFromTemplateRow(this._row, "answer_list", []); - this.arrayOfBtn = this.createArrayBtnElement(this.answer_list); + this.answerList = getAnswerListParamFromTemplateRow(this._row, "answer_list", []); + this.arrayOfBtn = this.createArrayBtnElement(this.answerList); this.getFlexWidth(); this.groupName = this._row._nested_name; @@ -76,10 +76,9 @@ export class TmplRadioGroupComponent * ] * Convert to an object array, with key value pairs extracted from the string values */ - createArrayBtnElement(answer_list: string[]) { - if (answer_list) { - let arrayOfBtn = parseAnswerList(answer_list); - arrayOfBtn = arrayOfBtn.map((itemObj) => this.processButtonFields(itemObj)); + createArrayBtnElement(answerList: IAnswerListItem[]) { + if (answerList) { + const arrayOfBtn = answerList.map((itemObj) => this.processButtonFields(itemObj)); // TODO - CC 2023-03-15 could lead to strange behaviour, to review // (checks every item but keeps overriding the button type depending on what it finds) diff --git a/src/app/shared/utils/utils.ts b/src/app/shared/utils/utils.ts index 0596e6a8f2..80f646d488 100644 --- a/src/app/shared/utils/utils.ts +++ b/src/app/shared/utils/utils.ts @@ -209,6 +209,71 @@ export function getBooleanParamFromTemplateRow( return params.hasOwnProperty(name) ? params[name] === "true" : _default; } +export function getAnswerListParamFromTemplateRow( + row: FlowTypes.TemplateRow, + name: string, + _default: IAnswerListItem[] +): IAnswerListItem[] { + const params = row.parameter_list || {}; + console.log(params[name]); + return params.hasOwnProperty(name) ? parseAnswerList(params[name]) : _default; +} + +export interface IAnswerListItem { + name: string; + image?: string; + text?: string; + image_checked?: string | null; +} + +/** + * Parse an answer_list parameter and return an array of AnswerListItems + * @param answerList an answer_list parameter, either an array of IAnswerListItems + * (possibly still in string representation) or a data list (hashmap of IAnswerListItems) + */ +function parseAnswerList(answerList: any) { + if (!answerList) return []; + // If a data_list (hashmap) is provided as input, convert to an array + if (answerList.constructor === {}.constructor) { + answerList = objectToArray(answerList); + } + const answerListItems: IAnswerListItem[] = answerList.map( + (item: string | Record) => { + return parseAnswerListItem(item); + } + ); + // Remove any items from the list which only have a value for "name", + // e.g. "image" and "text" are undefined because the list has been generated within a template + const filteredAnswerListItems = answerListItems.filter((item: IAnswerListItem) => { + const hadItemData = Object.entries(item).find( + ([key, value]) => key !== "name" && value !== undefined + ); + return hadItemData ? true : false; + }); + return filteredAnswerListItems; +} + +/** + * Convert answer list item (string or object) to relevant mappings + * TODO - CC 2023-03-16 - should ideally convert in parsers instead of at runtime + */ +function parseAnswerListItem(item: any) { + const itemObj: IAnswerListItem = {} as any; + if (typeof item === "string") { + const stringProperties = item.split("|"); + stringProperties.forEach((s) => { + let [field, value] = s.split(":").map((v) => v.trim()); + if (field && value) { + if (value === "undefined") value = undefined; + itemObj[field] = value; + } + }); + // NOTE CC 2021-08-07 - allow passing of object, not just string for conversion + return itemObj; + } + return item; +} + /** * Evaluate a javascript expression in a safe context * @param expression string expression, e.g. "!true", "5 - 4" @@ -415,60 +480,6 @@ function supportsOptionalChaining() { return true; } -export interface IAnswerListItem { - name: string; - image?: string; - text?: string; - image_checked?: string | null; -} - -/** - * Parse an answer_list parameter and return an array of AnswerListItems - * @param answerList an answer_list parameter, either an array of IAnswerListItems - * (possibly still in string representation) or a data list (hashmap of IAnswerListItems) - */ -export function parseAnswerList(answerList: any) { - // If a data_list (hashmap) is provided as input, convert to an array - if (answerList.constructor === {}.constructor) { - answerList = objectToArray(answerList); - } - const answerListItems: IAnswerListItem[] = answerList.map( - (item: string | Record) => { - return parseAnswerListItem(item); - } - ); - // Remove any items from the list which only have a value for "name", - // e.g. "image" and "text" are undefined because the list has been generated within a template - const filteredAnswerListItems = answerListItems.filter((item: IAnswerListItem) => { - const hadItemData = Object.entries(item).find( - ([key, value]) => key !== "name" && value !== undefined - ); - return hadItemData ? true : false; - }); - return filteredAnswerListItems; -} - -/** - * Convert answer list item (string or object) to relevant mappings - * TODO - CC 2023-03-16 - should ideally convert in parsers instead of at runtime - */ -function parseAnswerListItem(item: any) { - const itemObj: IAnswerListItem = {} as any; - if (typeof item === "string") { - const stringProperties = item.split("|"); - stringProperties.forEach((s) => { - let [field, value] = s.split(":").map((v) => v.trim()); - if (field && value) { - if (value === "undefined") value = undefined; - itemObj[field] = value; - } - }); - // NOTE CC 2021-08-07 - allow passing of object, not just string for conversion - return itemObj; - } - return item; -} - /** * Compiles markdown to HTML synchronously. * Extends the renderer of "marked" plugin to ensure that links open in new tags.