Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: handle undefined values in answer lists #2204

Merged
merged 5 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
}
}

Expand All @@ -59,6 +60,7 @@ export class TmplComboBoxComponent
false
);
this.style = getStringParamFromTemplateRow(this._row, "style", "");
this.answerList = getAnswerListParamFromTemplateRow(this._row, "answer_list", []);
}

async openModal() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 */
Expand Down Expand Up @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import {
getNumberParamFromTemplateRow,
getParamFromTemplateRow,
getStringParamFromTemplateRow,
parseAnswerList,
IAnswerListItem,
getAnswerListParamFromTemplateRow,
} from "../../../../utils";
import { ReplaySubject } from "rxjs";

Expand All @@ -38,7 +38,7 @@ export class TmplRadioGroupComponent
flexWidth: string;

// Parameters
answer_list: string[];
answerList: IAnswerListItem[];
options_per_row: number;
radioButtonType: string | null;
style: string;
Expand All @@ -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;
Expand All @@ -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)
Expand Down
110 changes: 65 additions & 45 deletions src/app/shared/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit(non-blocking) can remove temp logs

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 [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice addition (although shame the git diff isn't showing very cleanly)
I think the rest of the code is the same as you updated previously, so looks good to me

// 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<string, string>) => {
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"
Expand Down Expand Up @@ -415,51 +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<string, string>) => {
return parseAnswerListItem(item);
}
);
return answerListItems;
}

/**
* 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) => {
const [field, value] = s.split(":").map((v) => v.trim());
if (field && value) {
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.
Expand Down
Loading