From a12fd5bfd07ec9cbfd72bab5cd13f237f9ecf83d Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 21 Feb 2024 09:15:58 -0800 Subject: [PATCH 01/12] refactor: parse answer_lists in template parser --- .../processors/flowParser/flowParser.ts | 2 +- .../flowParser/parsers/template.parser.ts | 35 +++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/flowParser.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/flowParser.ts index 8d556cfbbc..856e3af379 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/flowParser.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/flowParser.ts @@ -5,7 +5,7 @@ import { arrayToHashmap, groupJsonByKey, IContentsEntry } from "../../utils"; import BaseProcessor from "../base"; export class FlowParserProcessor extends BaseProcessor { - public cacheVersion = 20230818.3; + public cacheVersion = 20240220.2; public parsers: { [flowType in FlowTypes.FlowType]: Parsers.DefaultParser } = { data_list: new Parsers.DataListParser(this), diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts index 7cf6454f43..a8fa6ca20a 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts @@ -33,11 +33,10 @@ export class TemplateParser extends DefaultParser { } // track path to row when nested row._nested_name = nestedPath ? `${nestedPath}.${row.name}` : row.name; - // convert any variables (local/global) list or collection strings (e.g. 'my_list_1') if (row.value && typeof row.value === "string") { - if (row.name?.includes("_list") && row.value && typeof row.value === "string") { - row.value = parseAppDataListString(row.value); + if (row.name?.includes("_list")) { + row.value = this.parseTemplateList(row.value); } if (row.name?.includes("_collection") && row.value && typeof row.value === "string") { row.value = parseAppDataCollectionString(row.value); @@ -74,6 +73,36 @@ export class TemplateParser extends DefaultParser { return flowsWithOverrides; } + /** + * Ensure any local variables defined with `_list` in their name are correctly + * parsed into list format + * TODO - add tests + */ + private parseTemplateList(value: any) { + // Assume all falsy values indicate an empty array + if (!value) return []; + + // Assume any non-string values already parsed + if (typeof value !== "string") return value; + + // HACK - use list separator to infer whether an actual list or not + // E.g. avoid parsing reference `my_list : @local.some_other_list` + // TODO - make clear to authors new convention + if (!value.includes(";")) return value; + + // HACK - assume any list with | characters designed as parameter list + const isCollectionList = value.includes("|"); + + // convert to array + let parsed: any[] = parseAppDataListString(value); + + // map array elements if collection list + if (isCollectionList) { + parsed = parsed.map((el: string) => parseAppDataCollectionString(el, "|")); + } + return parsed; + } + private parseParameterList(parameterList: string[]) { const parameterObj: FlowTypes.TemplateRow["parameter_list"] = {}; parameterList.forEach((p) => { From 84266520e6f9c8f78483ab678e6643dcc3f8b727 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 21 Feb 2024 09:16:30 -0800 Subject: [PATCH 02/12] chore: remove legacy parsAppDataString --- .../convert/utils/app-data-string.utils.ts | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/utils/app-data-string.utils.ts b/packages/scripts/src/commands/app-data/convert/utils/app-data-string.utils.ts index 0a820a9a2d..4190d78f08 100644 --- a/packages/scripts/src/commands/app-data/convert/utils/app-data-string.utils.ts +++ b/packages/scripts/src/commands/app-data/convert/utils/app-data-string.utils.ts @@ -1,33 +1,6 @@ -import chalk from "chalk"; import { FlowTypes } from "data-models"; import { setNestedProperty, booleanStringToBoolean } from "../utils"; -/** - * Xls data represents nested objects in the following ways - * ';' - pre-processing with '_list' columns to format as array - * '|' - post-processing specific item into set of arguments / parameters - * ':' - modifiers or properties of an argument - * - * As the pipe and colon characters may or may not exist for a particular string - * it is impossible to know any given data needs to be formatted as string or array - * to remain consistent with the rest of the column. As such all strings will be - * treated as arrays, and deeply nested objects extracted in future processing stages - * - * original: db_lookup:first |app_events:event_id | app_launch | before:7:day' - * nest 1: [db_lookup:first ,app_events:event_id , app_launch , before:7:day] - * nest 2: [[db_lookup,first] ,[app_events,event_id] , [app_launch] , [before,7,day]] - * - */ -export function parsAppDataString(str: string): string[][] { - if (str.includes(";")) { - console.error(chalk.red('lists should be pre-processed, but ";" found')); - process.exit(1); - } - const nest1 = str.split("|").map((d) => d.trim()); - const nest2 = nest1.map((el) => el.split(":").map((d) => d.trim())); - return nest2; -} - /** * Convert app data map string to object * @param str list string with key-value pairs, e.g From e4e57bc5463b32e0297626c81ee6023752969d49 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 21 Feb 2024 09:17:14 -0800 Subject: [PATCH 03/12] chore: update template answer list extract --- src/app/shared/utils/utils.ts | 33 ++++++--------------------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/src/app/shared/utils/utils.ts b/src/app/shared/utils/utils.ts index 04ae22ae5e..9e56be3f65 100644 --- a/src/app/shared/utils/utils.ts +++ b/src/app/shared/utils/utils.ts @@ -425,39 +425,18 @@ export interface IAnswerListItem { /** * 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) + * 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); - } - ); - 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; + return (answerList as IAnswerListItem[]).filter((item: IAnswerListItem) => { + // remove items where there are no values defined (excluding name) + const { name, ...answerItem } = item; + return Object.values(answerItem).find((v) => v !== undefined); + }); } /** From 974c65f49939817762b8ba4bd6265da8ce950bbe Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 22 Feb 2024 10:19:17 -0800 Subject: [PATCH 04/12] chore: wip spec tests --- .../parsers/template.parser.spec.ts | 181 +++++++++++++++++- 1 file changed, 179 insertions(+), 2 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.spec.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.spec.ts index 74cd1f1d51..6f923f92e5 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.spec.ts @@ -1,3 +1,180 @@ -describe("template Parser", () => { - it("TODO - add tests", () => expect(true).toEqual(true)); +import { FlowTypes } from "data-models"; +import { TemplateParser } from "./template.parser"; + +const ROW_BASE: FlowTypes.TemplateRow = { + _nested_name: "", + name: "", + type: "" as any, +}; + +describe("Template Parser PostProcessor", () => { + let parser: TemplateParser; + beforeEach(() => { + parser = new TemplateParser({} as any); + }); + + it("Remove empty rows", () => { + const res = parser.postProcessRow({} as any); + expect(res).toEqual(undefined); + }); + + it("Replaces empty row type with set_variable", () => { + const res = parser.postProcessRow({ ...ROW_BASE }); + expect(res).toEqual({ ...ROW_BASE, type: "set_variable" }); + }); + + // TODO - notify authors of change (can now auto gen multiple) + it("Generates default row names when omitted for multiple rows", () => { + const res = parser.run({ + flow_name: "test_name_generate_multiple", + flow_type: "template", + rows: [ + { ...ROW_BASE, name: "", type: "accordion" }, + { ...ROW_BASE, name: "", type: "accordion" }, + ], + }); + expect(res.rows).toEqual([ + { + _nested_name: "accordion_1", + name: "accordion_1", + type: "accordion", + }, + { + _nested_name: "accordion_2", + name: "accordion_2", + type: "accordion", + }, + ]); + }); + + it("Converts rows with _list in name to templated list", () => { + // CASE 0 - Do not convert rows without _list in name + const case0 = parser.postProcessRow({ + ...ROW_BASE, + value: "item_1; item_2; item_3;", + }); + expect(case0.value).toEqual("item_1; item_2; item_3;"); + + // CASE 1 - List items defined inline + const case1 = parser.postProcessRow({ + ...ROW_BASE, + name: "test_list", + value: "item_1; item_2; item_3;", + }); + expect(case1.value).toEqual(["item_1", "item_2", "item_3"]); + // CASE 2 - List refers to variable (do not parse) + const case2 = parser.postProcessRow({ + ...ROW_BASE, + name: "test_list", + value: "@local.some_list", + }); + expect(case2.value).toEqual("@local.some_list"); + // CASE 3 - List items include variables + const case3 = parser.postProcessRow({ + ...ROW_BASE, + name: "test_list", + value: "@local.item_1; @local.item_2", + }); + expect(case3.value).toEqual(["@local.item_1", "@local.item_2"]); + // CASE 4 - List items as json (mix dynamic and missing) + const case4 = parser.postProcessRow({ + ...ROW_BASE, + name: "test_list", + value: "key_1a: textValue | key_1b: @local.value; key_2a: | key_2b: 5", + }); + expect(case4.value).toEqual([ + { key_1a: "textValue", key_1b: "@local.value" }, + { key_2a: "", key_2b: "5" }, + ]); + }); + + it("Converts rows with _collection in name to templated collection", () => { + const res = parser.postProcessRow({ + ...ROW_BASE, + name: "test_collection", + value: "key_1:value_1; key_2:value_2", + }); + expect(res.value).toEqual({ key_1: "value_1", key_2: "value_2" }); + }); + + it("Parses parameter lists", () => { + const res = parser.postProcessRow({ + ...ROW_BASE, + // NOTE - list already array due to defaultParser initial parse + parameter_list: ["key_1:val_1", "key_2: val_trailing_spaces ", "key_3_no_value"] as any, + }); + // TODO - anymore advanced cases? + expect(res.parameter_list).toEqual({ + key_1: "val_1", + key_2: "val_trailing_spaces", + key_3_no_value: "true", + }); + }); + + it("Replaces action list self-references", () => { + const res = parser.postProcessRow({ + ...ROW_BASE, + name: "my_action_list", + action_list: [ + { trigger: "click", action_id: "", args: ["@local.my_action_list", "some_value"] }, + ], + }); + console.log("action", res.action_list); + expect(res.action_list[0].args).toEqual(["this.value", "some_value"]); + }); + + it("Extracts dynamic fields", () => { + const res = parser.postProcessRow({ + ...ROW_BASE, + value: "@local.dynamic_value", + }); + expect(res._dynamicFields).toEqual({ + value: [ + { + fullExpression: "@local.dynamic_value", + matchedExpression: "@local.dynamic_value", + type: "local", + fieldName: "dynamic_value", + }, + ], + }); + }); + + it("Extracts dynamic dependencies", () => { + const res = parser.postProcessRow({ + ...ROW_BASE, + value: "@local.dynamic_value", + }); + expect(res._dynamicDependencies).toEqual({ "@local.dynamic_value": ["value"] }); + }); + + fit("Creates nested path names for child rows", () => { + const rows: FlowTypes.TemplateRow[] = [ + { + _nested_name: "my_items", + name: "my_items", + type: "items", + rows: [{ name: "nested_text", type: "text", _nested_name: "nested_text" }], + }, + ]; + const res = parser.run({ flow_type: "template", flow_name: "test_nested", rows }); + const itemRows = res.rows[0].rows; + console.log("itemRows", itemRows); + expect(itemRows).toEqual([ + { name: "nested_text", type: "text", _nested_name: "my_items.nested_text" }, + ]); + // TODO - include auto-gen nested names + }); +}); + +describe("Template Parser [QC]", () => { + it("Ensures answer_list parameters refer to list variables", () => { + const res = parser.postProcessRow({ ...ROW_BASE }); + expect(res).toEqual({ ...ROW_BASE }); + }); + + it("Warns if ", () => { + const res = parser.postProcessRow({ ...ROW_BASE }); + expect(res).toEqual({ ...ROW_BASE }); + }); }); From fa76774dded92637979794849c9d936697a43377 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 22 Feb 2024 10:20:43 -0800 Subject: [PATCH 05/12] feat: template row name generation --- .../flowParser/parsers/default.parser.ts | 7 +++++-- .../flowParser/parsers/template.parser.ts | 19 ++++++++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts index 9ec9ddede3..9f0f5def68 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts @@ -44,13 +44,16 @@ export class DefaultParser< this.queue.shift(); } // Process queue + let rowIndex = 0; while (this.queue.length > 0) { + // Start rowIndex from 1 to match sheet (without header row) + rowIndex++; const row = this.queue[0]; try { const processed = new RowProcessor(row, this, rowDefaultValues).run(); // some rows may be omitted during processing so ignore if (processed) { - const postProcessed = this.postProcessRow(processed); + const postProcessed = this.postProcessRow(processed, rowIndex); if (postProcessed) { processedRows.push(postProcessed); } @@ -87,7 +90,7 @@ export class DefaultParser< /** Overridable method called by parser to apply any additional processing * on each individual row. By default the original row is simply returned */ - public postProcessRow(row: any) { + public postProcessRow(row: any, index: number) { return row; } diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts index a8fa6ca20a..26559c5688 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts @@ -9,7 +9,7 @@ import { } from "../../../utils"; export class TemplateParser extends DefaultParser { - postProcessRow(row: FlowTypes.TemplateRow, nestedPath?: string) { + postProcessRow(row: FlowTypes.TemplateRow, index = 1, nestedPath?: string, nestedIndex?: number) { // remove empty rows if (Object.keys(row).length === 0) { return; @@ -28,12 +28,13 @@ export class TemplateParser extends DefaultParser { if (row.type === "template") { row.name = row.value; } else { - row.name = row.type; + row.name = `${row.type}_${index}`; } } // track path to row when nested row._nested_name = nestedPath ? `${nestedPath}.${row.name}` : row.name; // convert any variables (local/global) list or collection strings (e.g. 'my_list_1') + // in similar way to how top-level properties get converted by default parser if (row.value && typeof row.value === "string") { if (row.name?.includes("_list")) { row.value = this.parseTemplateList(row.value); @@ -57,7 +58,7 @@ export class TemplateParser extends DefaultParser { // handle nested rows in same way if (row.rows) { - row.rows = row.rows.map((r) => this.postProcessRow(r, row._nested_name)); + row.rows = row.rows.map((r, i) => this.postProcessRow(r, index, row._nested_name, i)); } if (row.exclude_from_translation) { @@ -65,6 +66,11 @@ export class TemplateParser extends DefaultParser { row.exclude_from_translation as any ); } + + const errors = this.qualityControlCheck(row); + if (errors.length > 0) { + throw JSON.stringify(errors, null, 2); + } return row; } @@ -103,6 +109,7 @@ export class TemplateParser extends DefaultParser { return parsed; } + /** Convert parameter list string array (as provided by default parser) to key-value pairs */ private parseParameterList(parameterList: string[]) { const parameterObj: FlowTypes.TemplateRow["parameter_list"] = {}; parameterList.forEach((p) => { @@ -145,4 +152,10 @@ export class TemplateParser extends DefaultParser { return action; }); } + + private qualityControlCheck(row: FlowTypes.TemplateRow) { + const errors: string[] = []; + + return errors; + } } From 7fdf475ec6cdb1a385ab18e6b54756185539241d Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 22 Feb 2024 10:43:16 -0800 Subject: [PATCH 06/12] chore: deprecate parseAnswerListItem --- src/app/shared/utils/utils.ts | 42 ++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/src/app/shared/utils/utils.ts b/src/app/shared/utils/utils.ts index 7be72e352a..1703ec92b1 100644 --- a/src/app/shared/utils/utils.ts +++ b/src/app/shared/utils/utils.ts @@ -2,6 +2,7 @@ import { format } from "date-fns"; import { diff } from "deep-object-diff"; import { Observable } from "rxjs"; import { map, pairwise, filter, share } from "rxjs/operators"; +import * as Sentry from "@sentry/angular"; import { FlowTypes } from "../model"; import { objectToArray } from "../components/template/utils"; import marked from "marked"; @@ -240,12 +241,41 @@ 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 - return (answerList as IAnswerListItem[]).filter((item: IAnswerListItem) => { - const hadItemData = Object.entries(item).find( - ([key, value]) => key !== "name" && value !== undefined - ); - return hadItemData ? true : false; - }); + return (answerList as IAnswerListItem[]) + .map((item) => parseAnswerListItem(item)) + .filter((item: IAnswerListItem) => { + const hadItemData = Object.entries(item).find( + ([key, value]) => key !== "name" && value !== undefined + ); + return hadItemData ? true : false; + }); +} + +/** + * @deprecated 2024-02-22 main method moved to parser however may still be required for edge cases + * where user passes answer list directly to parameter_list, or uses local variable not named `_list` + * + * Convert answer list item (string or object) to relevant mappings + */ +function parseAnswerListItem(item: any) { + const itemObj: IAnswerListItem = {} as any; + if (typeof item === "string") { + const msg = + "Unexpected answerList item as string. It should be set from local with '_list' in name"; + Sentry.captureException(msg, { extra: { item } }); + console.warn(msg, item); + 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 || {}; } /** From a87acd3b8682e927f3a94584856b2e0d578c6b5a Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 22 Feb 2024 10:55:37 -0800 Subject: [PATCH 07/12] chore: code tidying --- .../processors/flowParser/parsers/default.parser.ts | 10 +++++----- .../flowParser/parsers/template.parser.ts | 13 +++++++++---- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts index 9f0f5def68..1232577422 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts @@ -44,16 +44,16 @@ export class DefaultParser< this.queue.shift(); } // Process queue - let rowIndex = 0; + let rowNumber = 1; while (this.queue.length > 0) { - // Start rowIndex from 1 to match sheet (without header row) - rowIndex++; + // Start rowNumber from 2 to match sheet (without header row) + rowNumber++; const row = this.queue[0]; try { const processed = new RowProcessor(row, this, rowDefaultValues).run(); // some rows may be omitted during processing so ignore if (processed) { - const postProcessed = this.postProcessRow(processed, rowIndex); + const postProcessed = this.postProcessRow(processed, rowNumber); if (postProcessed) { processedRows.push(postProcessed); } @@ -90,7 +90,7 @@ export class DefaultParser< /** Overridable method called by parser to apply any additional processing * on each individual row. By default the original row is simply returned */ - public postProcessRow(row: any, index: number) { + public postProcessRow(row: any, rowNumber: number) { return row; } diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts index 26559c5688..b84cdbaafc 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts @@ -9,7 +9,12 @@ import { } from "../../../utils"; export class TemplateParser extends DefaultParser { - postProcessRow(row: FlowTypes.TemplateRow, index = 1, nestedPath?: string, nestedIndex?: number) { + postProcessRow( + row: FlowTypes.TemplateRow, + rowNumber = 2, + nestedPath?: string, + nestedRowNumber?: number + ) { // remove empty rows if (Object.keys(row).length === 0) { return; @@ -28,11 +33,11 @@ export class TemplateParser extends DefaultParser { if (row.type === "template") { row.name = row.value; } else { - row.name = `${row.type}_${index}`; + row.name = `${row.type}_${rowNumber}`; } } // track path to row when nested - row._nested_name = nestedPath ? `${nestedPath}.${row.name}` : row.name; + row._nested_name = nestedPath ? `${nestedPath}.${row.name}_${nestedRowNumber}` : row.name; // convert any variables (local/global) list or collection strings (e.g. 'my_list_1') // in similar way to how top-level properties get converted by default parser if (row.value && typeof row.value === "string") { @@ -58,7 +63,7 @@ export class TemplateParser extends DefaultParser { // handle nested rows in same way if (row.rows) { - row.rows = row.rows.map((r, i) => this.postProcessRow(r, index, row._nested_name, i)); + row.rows = row.rows.map((r, i) => this.postProcessRow(r, rowNumber, row._nested_name, i + 1)); } if (row.exclude_from_translation) { From b6c9dc86d4fcb224faad56ce2059601f1234243b Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 22 Feb 2024 11:42:30 -0800 Subject: [PATCH 08/12] chore: code tidying --- .../parsers/template.parser.spec.ts | 45 +++++++++---------- .../flowParser/parsers/template.parser.ts | 12 ++--- 2 files changed, 25 insertions(+), 32 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.spec.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.spec.ts index 6f923f92e5..4ef2f6c6db 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.spec.ts @@ -19,8 +19,8 @@ describe("Template Parser PostProcessor", () => { }); it("Replaces empty row type with set_variable", () => { - const res = parser.postProcessRow({ ...ROW_BASE }); - expect(res).toEqual({ ...ROW_BASE, type: "set_variable" }); + const res = parser.postProcessRow({ ...ROW_BASE, type: "" as any }); + expect(res.type).toEqual("set_variable"); }); // TODO - notify authors of change (can now auto gen multiple) @@ -34,14 +34,15 @@ describe("Template Parser PostProcessor", () => { ], }); expect(res.rows).toEqual([ + // Note - suffix numbers start at 2 as flow sheet would typically have row 1 as header row { - _nested_name: "accordion_1", - name: "accordion_1", + _nested_name: "accordion_2", + name: "accordion_2", type: "accordion", }, { - _nested_name: "accordion_2", - name: "accordion_2", + _nested_name: "accordion_3", + name: "accordion_3", type: "accordion", }, ]); @@ -119,7 +120,6 @@ describe("Template Parser PostProcessor", () => { { trigger: "click", action_id: "", args: ["@local.my_action_list", "some_value"] }, ], }); - console.log("action", res.action_list); expect(res.action_list[0].args).toEqual(["this.value", "some_value"]); }); @@ -148,33 +148,32 @@ describe("Template Parser PostProcessor", () => { expect(res._dynamicDependencies).toEqual({ "@local.dynamic_value": ["value"] }); }); - fit("Creates nested path names for child rows", () => { + it("Creates nested path names for child rows", () => { const rows: FlowTypes.TemplateRow[] = [ { _nested_name: "my_items", name: "my_items", type: "items", - rows: [{ name: "nested_text", type: "text", _nested_name: "nested_text" }], + // Handle case of both named and unnamed nested row names + rows: [ + { + ...ROW_BASE, + type: "text", + name: "named_text", + }, + { ...ROW_BASE, type: "text" }, + ], }, ]; const res = parser.run({ flow_type: "template", flow_name: "test_nested", rows }); const itemRows = res.rows[0].rows; - console.log("itemRows", itemRows); - expect(itemRows).toEqual([ - { name: "nested_text", type: "text", _nested_name: "my_items.nested_text" }, - ]); - // TODO - include auto-gen nested names + const nestedNames = itemRows.map((n) => n._nested_name); + expect(nestedNames).toEqual(["my_items.named_text", "my_items.text_2"]); }); }); describe("Template Parser [QC]", () => { - it("Ensures answer_list parameters refer to list variables", () => { - const res = parser.postProcessRow({ ...ROW_BASE }); - expect(res).toEqual({ ...ROW_BASE }); - }); - - it("Warns if ", () => { - const res = parser.postProcessRow({ ...ROW_BASE }); - expect(res).toEqual({ ...ROW_BASE }); - }); + // TODO - confirm what checks to include and add to code + // it("Ensures answer_list parameters refer to list variables", () => { + // }); }); diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts index b84cdbaafc..1957014930 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts @@ -9,12 +9,7 @@ import { } from "../../../utils"; export class TemplateParser extends DefaultParser { - postProcessRow( - row: FlowTypes.TemplateRow, - rowNumber = 2, - nestedPath?: string, - nestedRowNumber?: number - ) { + postProcessRow(row: FlowTypes.TemplateRow, rowNumber = 1, nestedPath?: string) { // remove empty rows if (Object.keys(row).length === 0) { return; @@ -37,7 +32,7 @@ export class TemplateParser extends DefaultParser { } } // track path to row when nested - row._nested_name = nestedPath ? `${nestedPath}.${row.name}_${nestedRowNumber}` : row.name; + row._nested_name = nestedPath ? `${nestedPath}.${row.name}` : row.name; // convert any variables (local/global) list or collection strings (e.g. 'my_list_1') // in similar way to how top-level properties get converted by default parser if (row.value && typeof row.value === "string") { @@ -63,7 +58,7 @@ export class TemplateParser extends DefaultParser { // handle nested rows in same way if (row.rows) { - row.rows = row.rows.map((r, i) => this.postProcessRow(r, rowNumber, row._nested_name, i + 1)); + row.rows = row.rows.map((r, i) => this.postProcessRow(r, i + 1, row._nested_name)); } if (row.exclude_from_translation) { @@ -87,7 +82,6 @@ export class TemplateParser extends DefaultParser { /** * Ensure any local variables defined with `_list` in their name are correctly * parsed into list format - * TODO - add tests */ private parseTemplateList(value: any) { // Assume all falsy values indicate an empty array From 133f3b69af7962fa4315b9789ce6355f0e1537bc Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Fri, 15 Mar 2024 08:51:51 -0700 Subject: [PATCH 09/12] chore: deprecate and move nav bar value list item parse --- .../navigation-bar.component.ts | 32 ++++++++++++++++++- .../template/utils/template-utils.ts | 27 ---------------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/app/shared/components/template/components/navigation-bar/navigation-bar.component.ts b/src/app/shared/components/template/components/navigation-bar/navigation-bar.component.ts index 960a0444ca..4d24278873 100644 --- a/src/app/shared/components/template/components/navigation-bar/navigation-bar.component.ts +++ b/src/app/shared/components/template/components/navigation-bar/navigation-bar.component.ts @@ -1,7 +1,6 @@ import { Component, OnInit } from "@angular/core"; import { TemplateBaseComponent } from "../base"; import { getParamFromTemplateRow } from "src/app/shared/utils"; -import { parseValueListItems } from "../../utils"; interface IButton { image: string | null; @@ -27,3 +26,34 @@ export class TmplNavigationBarComponent extends TemplateBaseComponent implements this.buttonList = parseValueListItems(buttonListRaw); } } + +/** + * Radio group and other value lists are often formatted with both name and text properties, + * which are (currently) separated out at runtime. This function takes the value_list + * string array and formats into an object array + * @param items + * ``` + * [name:name_var_1 | text:Option 1, name:name_var_2 | text: Option 2] + * ``` + * @returns + * ``` + * [{name:"name_var_1", text:"Option 1"}, {name:"name_var_2", text: "Option 2"}] + * ``` + * @deprecated v0.16.27 - PR 2211 should have enabled support for parsing at compile time + * have added return in case already processed but should hopefully be able to remove in future + */ +function parseValueListItems(items: string[]): any[] { + return items.map((item) => { + // avoid parsing items that may have been processed already + if (typeof item !== "string") return item; + const props = {}; + item + .split("|") + .map((i) => i.trim()) + .forEach((i) => { + const [key, value] = i.split(":").map((v) => v.trim()); + props[key] = value; + }); + return props; + }); +} diff --git a/src/app/shared/components/template/utils/template-utils.ts b/src/app/shared/components/template/utils/template-utils.ts index 3af1c50fc8..af9b870793 100644 --- a/src/app/shared/components/template/utils/template-utils.ts +++ b/src/app/shared/components/template/utils/template-utils.ts @@ -89,33 +89,6 @@ function flattenJson(json: any, tree = {}, nestedPath?: string): { [key: stri return tree; } -/** - * Radio group and other value lists are often formatted with both name and text properties, - * which are (currently) separated out at runtime. This function takes the value_list - * string array and formats into an object array - * @param items - * ``` - * [name:name_var_1 | text:Option 1, name:name_var_2 | text: Option 2] - * ``` - * @returns - * ``` - * [{name:"name_var_1", text:"Option 1"}, {name:"name_var_2", text: "Option 2"}] - * ``` - */ -export function parseValueListItems(items: string[]): any[] { - return items.map((item) => { - const props = {}; - item - .split("|") - .map((i) => i.trim()) - .forEach((i) => { - const [key, value] = i.split(":").map((v) => v.trim()); - props[key] = value; - }); - return props; - }); -} - /** * Take an object and return an array via the object.values method. * Provide additional check in case already is array (return array), or is not an object From 46413a73ee49b02a5b0f2d4377e3eb8504b029e1 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Fri, 15 Mar 2024 09:14:55 -0700 Subject: [PATCH 10/12] chore: code tidying --- .../processors/flowParser/flowParser.ts | 2 +- .../flowParser/parsers/template.parser.ts | 18 +++++++++++++----- src/app/shared/utils/utils.ts | 2 +- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/flowParser.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/flowParser.ts index 856e3af379..eb2415778e 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/flowParser.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/flowParser.ts @@ -5,7 +5,7 @@ import { arrayToHashmap, groupJsonByKey, IContentsEntry } from "../../utils"; import BaseProcessor from "../base"; export class FlowParserProcessor extends BaseProcessor { - public cacheVersion = 20240220.2; + public cacheVersion = 20240315.0; public parsers: { [flowType in FlowTypes.FlowType]: Parsers.DefaultParser } = { data_list: new Parsers.DataListParser(this), diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts index 1957014930..75764ebee8 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts @@ -25,11 +25,7 @@ export class TemplateParser extends DefaultParser { // when unique name not specified assume namespacing with template value (flow_name) // or the row type for non-templates if (!row.name) { - if (row.type === "template") { - row.name = row.value; - } else { - row.name = `${row.type}_${rowNumber}`; - } + row.name = this.generateRowName(row, rowNumber); } // track path to row when nested row._nested_name = nestedPath ? `${nestedPath}.${row.name}` : row.name; @@ -152,6 +148,18 @@ export class TemplateParser extends DefaultParser { }); } + /** Automatically generate a row name when not provided by author */ + private generateRowName(row: FlowTypes.TemplateRow, rowNumber: Number) { + switch (row.type) { + // template row name assigned to target template name + case "template": + return row.value; + // default use combination of row type and row number + default: + return `${row.type}_${rowNumber}`; + } + } + private qualityControlCheck(row: FlowTypes.TemplateRow) { const errors: string[] = []; diff --git a/src/app/shared/utils/utils.ts b/src/app/shared/utils/utils.ts index 1703ec92b1..432a586766 100644 --- a/src/app/shared/utils/utils.ts +++ b/src/app/shared/utils/utils.ts @@ -2,7 +2,7 @@ import { format } from "date-fns"; import { diff } from "deep-object-diff"; import { Observable } from "rxjs"; import { map, pairwise, filter, share } from "rxjs/operators"; -import * as Sentry from "@sentry/angular"; +import * as Sentry from "@sentry/angular-ivy"; import { FlowTypes } from "../model"; import { objectToArray } from "../components/template/utils"; import marked from "marked"; From ea119d9463444f52277095baeea4dfa7ad378dca Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Tue, 2 Apr 2024 18:36:25 -0700 Subject: [PATCH 11/12] feat: update list and collection field matching --- .../convert/processors/flowParser/flowParser.ts | 3 ++- .../flowParser/parsers/default.parser.ts | 4 ++-- .../flowParser/parsers/template.parser.spec.ts | 15 ++++++++------- .../flowParser/parsers/template.parser.ts | 8 +++++--- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/flowParser.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/flowParser.ts index eb2415778e..2282b96fad 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/flowParser.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/flowParser.ts @@ -5,7 +5,7 @@ import { arrayToHashmap, groupJsonByKey, IContentsEntry } from "../../utils"; import BaseProcessor from "../base"; export class FlowParserProcessor extends BaseProcessor { - public cacheVersion = 20240315.0; + public cacheVersion = 20240402.0; public parsers: { [flowType in FlowTypes.FlowType]: Parsers.DefaultParser } = { data_list: new Parsers.DataListParser(this), @@ -14,6 +14,7 @@ export class FlowParserProcessor extends BaseProcessor { ]); }); - it("Converts rows with _list in name to templated list", () => { - // CASE 0 - Do not convert rows without _list in name - const case0 = parser.postProcessRow({ + it("Converts rows ending _list or containing _list_ in name to templated list", () => { + // CASE 0 - Do not convert rows unless explicit includes _list_ or ends _list + const case0_1 = parser.postProcessRow({ ...ROW_BASE, + name: "test_listen", value: "item_1; item_2; item_3;", }); - expect(case0.value).toEqual("item_1; item_2; item_3;"); + expect(case0_1.value).toEqual("item_1; item_2; item_3;"); // CASE 1 - List items defined inline const case1 = parser.postProcessRow({ @@ -66,21 +67,21 @@ describe("Template Parser PostProcessor", () => { // CASE 2 - List refers to variable (do not parse) const case2 = parser.postProcessRow({ ...ROW_BASE, - name: "test_list", + name: "test_list_2", value: "@local.some_list", }); expect(case2.value).toEqual("@local.some_list"); // CASE 3 - List items include variables const case3 = parser.postProcessRow({ ...ROW_BASE, - name: "test_list", + name: "test_list_3", value: "@local.item_1; @local.item_2", }); expect(case3.value).toEqual(["@local.item_1", "@local.item_2"]); // CASE 4 - List items as json (mix dynamic and missing) const case4 = parser.postProcessRow({ ...ROW_BASE, - name: "test_list", + name: "test_list_4", value: "key_1a: textValue | key_1b: @local.value; key_2a: | key_2b: 5", }); expect(case4.value).toEqual([ diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts index 75764ebee8..ec363dff6c 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts @@ -32,11 +32,13 @@ export class TemplateParser extends DefaultParser { // convert any variables (local/global) list or collection strings (e.g. 'my_list_1') // in similar way to how top-level properties get converted by default parser if (row.value && typeof row.value === "string") { - if (row.name?.includes("_list")) { + if (row.name?.endsWith("_list") || row.name?.includes("_list_")) { row.value = this.parseTemplateList(row.value); } - if (row.name?.includes("_collection") && row.value && typeof row.value === "string") { - row.value = parseAppDataCollectionString(row.value); + if (row.name?.endsWith("_collection") || row.name?.includes("_collection_")) { + if (row.value && typeof row.value === "string") { + row.value = parseAppDataCollectionString(row.value); + } } } if (row.parameter_list) { From 0089892db538ff6ac8083c37dad0ee37e363712a Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Fri, 12 Apr 2024 12:26:46 -0700 Subject: [PATCH 12/12] fix: cache versions --- .../src/commands/app-data/convert/processors/base.ts | 9 +++------ .../app-data/convert/processors/flowParser/flowParser.ts | 6 +++--- .../commands/app-data/convert/processors/xlsxWorkbook.ts | 5 +++-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/processors/base.ts b/packages/scripts/src/commands/app-data/convert/processors/base.ts index c384844c44..bbf674888a 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/base.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/base.ts @@ -9,9 +9,6 @@ import { JsonFileCache } from "../cacheStrategy/jsonFile"; import chalk from "chalk"; class BaseProcessor { - /** Used to invalidate cache */ - public cacheVersion = 20231001.0; - public logger: Logger; public cache: JsonFileCache; @@ -26,7 +23,7 @@ class BaseProcessor { * Create a base processor instance. Sets up logging and cache * @param context.namespace - Name used as prefix for cache and logging */ - constructor(public context: { namespace: string; paths: IConverterPaths }) { + constructor(public context: { namespace: string; paths: IConverterPaths; cacheVersion: number }) { const { namespace } = context; this.logger = createChildFileLogger({ source: namespace }); this.setupCache(); @@ -36,9 +33,9 @@ class BaseProcessor { * included relative path, md5 checksum and modified time */ private setupCache() { - const { paths, namespace } = this.context; + const { paths, namespace, cacheVersion } = this.context; const cacheFolder = path.resolve(paths.SHEETS_CACHE_FOLDER, namespace); - this.cache = new JsonFileCache(cacheFolder, this.cacheVersion); + this.cache = new JsonFileCache(cacheFolder, cacheVersion); } /** diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/flowParser.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/flowParser.ts index 2282b96fad..c0c162a950 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/flowParser.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/flowParser.ts @@ -4,9 +4,9 @@ import { IConverterPaths, IFlowHashmapByType, IParsedWorkbookData } from "../../ import { arrayToHashmap, groupJsonByKey, IContentsEntry } from "../../utils"; import BaseProcessor from "../base"; -export class FlowParserProcessor extends BaseProcessor { - public cacheVersion = 20240402.0; +const cacheVersion = 20240412.0; +export class FlowParserProcessor extends BaseProcessor { public parsers: { [flowType in FlowTypes.FlowType]: Parsers.DefaultParser } = { data_list: new Parsers.DataListParser(this), data_pipe: new Parsers.DataPipeParser(this), @@ -31,7 +31,7 @@ export class FlowParserProcessor extends BaseProcessor { - public cacheVersion = 20230509.1; constructor(paths: IConverterPaths) { - super({ paths, namespace: "xlsxWorkbookProcessor" }); + super({ paths, namespace: "xlsxWorkbookProcessor", cacheVersion }); } public async processInput(entry: IContentsEntry) {