From 882cc4e16431601c215674425cfc2f12b556d9c1 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 1 Mar 2024 10:43:31 +0000 Subject: [PATCH 01/30] wip: set_item_at_index action --- packages/data-models/flowTypes.ts | 1 + .../data-items/data-items.component.ts | 12 +++++++++ .../dynamic-data/dynamic-data.service.ts | 26 ++++++++++++++++++- 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/data-models/flowTypes.ts b/packages/data-models/flowTypes.ts index 84b00c5214..1fa3f58b5f 100644 --- a/packages/data-models/flowTypes.ts +++ b/packages/data-models/flowTypes.ts @@ -392,6 +392,7 @@ export namespace FlowTypes { "save_to_device", "set_field", "set_item", + "set_item_at_index", "set_items", "set_local", "share", diff --git a/src/app/shared/components/template/components/data-items/data-items.component.ts b/src/app/shared/components/template/components/data-items/data-items.component.ts index ce822d25cc..1c71e598db 100644 --- a/src/app/shared/components/template/components/data-items/data-items.component.ts +++ b/src/app/shared/components/template/components/data-items/data-items.component.ts @@ -80,6 +80,13 @@ export class TmplDataItemsComponent extends TemplateBaseComponent implements OnD // TODO - deep diff and only update changed this.itemRows = replacedActionRows; this.cdr.markForCheck(); + // Without this second check, and without the delay, the nested template does not render. Must be something async somewhere but I'm not sure what + await new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 500); + }); + this.cdr.markForCheck(); } /** @@ -103,6 +110,11 @@ export class TmplDataItemsComponent extends TemplateBaseComponent implements OnD // for each individual item (default will be just to pick the first) a.args = [this.dataListName, Object.values(dataList).map((v) => v.id)]; } + if (a.action_id === "set_item_at_index") { + const row_ids = Object.values(dataList).map((v) => v.id); + const currentItemIndex = row_ids.indexOf(r._evalContext.itemContext.id); + a.args = a.args.concat([this.dataListName, currentItemIndex, row_ids]); + } return a; }); } diff --git a/src/app/shared/services/dynamic-data/dynamic-data.service.ts b/src/app/shared/services/dynamic-data/dynamic-data.service.ts index 1f47c86473..e663ab33bc 100644 --- a/src/app/shared/services/dynamic-data/dynamic-data.service.ts +++ b/src/app/shared/services/dynamic-data/dynamic-data.service.ts @@ -6,7 +6,7 @@ import { FlowTypes } from "data-models"; import { environment } from "src/environments/environment"; import { AppDataService } from "../data/app-data.service"; import { AsyncServiceBase } from "../asyncService.base"; -import { arrayToHashmap, deepMergeObjects } from "../../utils"; +import { arrayToHashmap, deepMergeObjects, evaluateJSExpression } from "../../utils"; import { PersistedMemoryAdapter } from "./adapters/persistedMemory"; import { ReactiveMemoryAdapater, REACTIVE_SCHEMA_BASE } from "./adapters/reactiveMemory"; import { TemplateActionRegistry } from "../../components/template/services/instance/template-action.registry"; @@ -70,6 +70,16 @@ export class DynamicDataService extends AsyncServiceBase { const [flow_name, row_id] = args; await this.update("data_list", flow_name, row_id, params); }, + set_item_at_index: async ({ args, params }) => { + const [targetIndexString, flow_name, currentItemIndex, row_ids] = args; + const targetIndex = this.evaluateItemIndexString(targetIndexString, currentItemIndex); + const targetRowId = row_ids[targetIndex]; + if (targetRowId) { + await this.update("data_list", flow_name, targetRowId, params); + } else { + console.warn(`[SET ITEM AT INDEX] - No item at index ${targetIndex}`); + } + }, set_items: async ({ args, params }) => { const [flow_name, row_ids] = args; // Hack, no current method for bulk update so make successive (changes debounced in component) @@ -223,4 +233,18 @@ export class DynamicDataService extends AsyncServiceBase { } return schema; } + + evaluateItemIndexString(targetIndexString: string, currentIndex: number) { + if (isNaN(Number(targetIndexString))) { + try { + // HACK use keyword "item_index" to refer to index of current item in loop + const jsExpression = targetIndexString.replace("item_index", currentIndex.toString()); + return evaluateJSExpression(jsExpression); + } catch { + console.error(`[SET ITEM AT INDEX] - Invalid index selector, "${targetIndexString}"`); + } + } else { + return Number(targetIndexString); + } + } } From 33ac196607087f5bbd0c73624be218997ee5ecfe Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 1 Mar 2024 13:04:51 +0000 Subject: [PATCH 02/30] feat: add rest_data action --- packages/data-models/flowTypes.ts | 1 + src/app/shared/services/dynamic-data/dynamic-data.service.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/packages/data-models/flowTypes.ts b/packages/data-models/flowTypes.ts index 1fa3f58b5f..8b83153593 100644 --- a/packages/data-models/flowTypes.ts +++ b/packages/data-models/flowTypes.ts @@ -389,6 +389,7 @@ export namespace FlowTypes { "pop_up", "process_template", "reset_app", + "reset_data", "save_to_device", "set_field", "set_item", diff --git a/src/app/shared/services/dynamic-data/dynamic-data.service.ts b/src/app/shared/services/dynamic-data/dynamic-data.service.ts index e663ab33bc..78386e05ea 100644 --- a/src/app/shared/services/dynamic-data/dynamic-data.service.ts +++ b/src/app/shared/services/dynamic-data/dynamic-data.service.ts @@ -87,6 +87,10 @@ export class DynamicDataService extends AsyncServiceBase { await this.update("data_list", flow_name, row_id, params); } }, + // "Reset" the data of a given data list by removing its dynamic user overwrites + reset_data: async ({ args }) => { + this.resetFlow("data_list", args[0]); + }, }); } From a406ad24e1065600e5a69bd717df8165c350ec30 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 1 Mar 2024 13:10:21 +0000 Subject: [PATCH 03/30] chore: clarified comment --- .../template/components/data-items/data-items.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/shared/components/template/components/data-items/data-items.component.ts b/src/app/shared/components/template/components/data-items/data-items.component.ts index 1c71e598db..bc8d9c4821 100644 --- a/src/app/shared/components/template/components/data-items/data-items.component.ts +++ b/src/app/shared/components/template/components/data-items/data-items.component.ts @@ -80,7 +80,8 @@ export class TmplDataItemsComponent extends TemplateBaseComponent implements OnD // TODO - deep diff and only update changed this.itemRows = replacedActionRows; this.cdr.markForCheck(); - // Without this second check, and without the delay, the nested template does not render. Must be something async somewhere but I'm not sure what + // Without this second check, and without the delay, templates nested inside data-items loops do not render. + // There must be some async process somewhere but I'm not sure what it is await new Promise((resolve) => { setTimeout(() => { resolve(); From 11831a94cf58d67ca891ebb76faade6418212d42 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 5 Mar 2024 15:56:15 +0000 Subject: [PATCH 04/30] refactor: incorporate set_item_at_index logic into set_item instead --- packages/data-models/flowTypes.ts | 3 +- .../data-items/data-items.component.ts | 9 ++--- .../components/template/processors/item.ts | 2 +- .../services/template-variables.service.ts | 2 +- .../dynamic-data/dynamic-data.service.ts | 37 ++++++++++++++----- 5 files changed, 34 insertions(+), 19 deletions(-) diff --git a/packages/data-models/flowTypes.ts b/packages/data-models/flowTypes.ts index 8b83153593..300db7fc5e 100644 --- a/packages/data-models/flowTypes.ts +++ b/packages/data-models/flowTypes.ts @@ -338,8 +338,10 @@ export namespace FlowTypes { _dynamicDependencies?: { [reference: string]: string[] }; _translatedFields?: { [field: string]: any }; _evalContext?: { itemContext: any }; // force specific context variables when calculating eval statements (such as loop items) + _index?: number; // Added dynamically to some rows, i.e. those in item loops __EMPTY?: any; // empty cells (can be removed after pr 679 merged) } + export type IDynamicField = { [key: string]: IDynamicField | TemplateRowDynamicEvaluator[] }; type IDynamicPrefix = IAppConfig["DYNAMIC_PREFIXES"][number]; @@ -393,7 +395,6 @@ export namespace FlowTypes { "save_to_device", "set_field", "set_item", - "set_item_at_index", "set_items", "set_local", "share", diff --git a/src/app/shared/components/template/components/data-items/data-items.component.ts b/src/app/shared/components/template/components/data-items/data-items.component.ts index bc8d9c4821..2cd9808690 100644 --- a/src/app/shared/components/template/components/data-items/data-items.component.ts +++ b/src/app/shared/components/template/components/data-items/data-items.component.ts @@ -104,18 +104,15 @@ export class TmplDataItemsComponent extends TemplateBaseComponent implements OnD if (r.action_list) { r.action_list = r.action_list.map((a) => { if (a.action_id === "set_item") { - a.args = [this.dataListName, r._evalContext.itemContext.id]; + const row_ids = Object.values(dataList).map((v) => v.id); + const row_id = r._evalContext.itemContext.id; + a.args = [this.dataListName, row_ids, row_id]; } if (a.action_id === "set_items") { // TODO - add a check for @item refs and replace parameter list with correct values // for each individual item (default will be just to pick the first) a.args = [this.dataListName, Object.values(dataList).map((v) => v.id)]; } - if (a.action_id === "set_item_at_index") { - const row_ids = Object.values(dataList).map((v) => v.id); - const currentItemIndex = row_ids.indexOf(r._evalContext.itemContext.id); - a.args = a.args.concat([this.dataListName, currentItemIndex, row_ids]); - } return a; }); } diff --git a/src/app/shared/components/template/processors/item.ts b/src/app/shared/components/template/processors/item.ts index 61f8a31718..64990f119f 100644 --- a/src/app/shared/components/template/processors/item.ts +++ b/src/app/shared/components/template/processors/item.ts @@ -37,7 +37,7 @@ export class ItemProcessor { private generateLoopItemRows(templateRows: FlowTypes.TemplateRow[], items: any[]) { const loopItemRows: FlowTypes.TemplateRow[] = []; for (const [index, item] of Object.entries(items)) { - item._index = index; + item._index = Number(index); const evalContext = { itemContext: item }; for (const r of templateRows) { const itemRow = this.setRecursiveRowEvalContext(r, evalContext); diff --git a/src/app/shared/components/template/services/template-variables.service.ts b/src/app/shared/components/template/services/template-variables.service.ts index 75984760e8..90c41a5fe3 100644 --- a/src/app/shared/components/template/services/template-variables.service.ts +++ b/src/app/shared/components/template/services/template-variables.service.ts @@ -126,7 +126,7 @@ export class TemplateVariablesService extends AsyncServiceBase { **/ private shouldEvaluateField(fieldName: keyof FlowTypes.TemplateRow, omitFields: string[] = []) { if (omitFields.includes(fieldName)) return false; - if (fieldName.startsWith("_")) return false; + if (fieldName.startsWith("_") && fieldName !== "_index") return false; return true; } diff --git a/src/app/shared/services/dynamic-data/dynamic-data.service.ts b/src/app/shared/services/dynamic-data/dynamic-data.service.ts index 78386e05ea..58a4fa09a8 100644 --- a/src/app/shared/services/dynamic-data/dynamic-data.service.ts +++ b/src/app/shared/services/dynamic-data/dynamic-data.service.ts @@ -66,18 +66,35 @@ export class DynamicDataService extends AsyncServiceBase { } private registerTemplateActionHandlers() { this.templateActionRegistry.register({ + /** + * Write properties on the current item (default), or on an explicitly targeted item, + * e.g. + * click | set_item | completed:true; + * click | set_item | id: @item.id, completed:true; + * click | set_item | _index: @item._index + 1, completed:true; + */ set_item: async ({ args, params }) => { - const [flow_name, row_id] = args; - await this.update("data_list", flow_name, row_id, params); - }, - set_item_at_index: async ({ args, params }) => { - const [targetIndexString, flow_name, currentItemIndex, row_ids] = args; - const targetIndex = this.evaluateItemIndexString(targetIndexString, currentItemIndex); - const targetRowId = row_ids[targetIndex]; - if (targetRowId) { - await this.update("data_list", flow_name, targetRowId, params); + const [flow_name, row_ids, row_id] = args; + const { _index, id, ...writeableProps } = params; + + // Target current row if another target is not explicitly provided + let targetRowId = row_id; + if (_index) { + targetRowId = row_ids[_index]; + } + if (id) { + targetRowId = id; + } + + if (row_ids.includes(targetRowId)) { + await this.update("data_list", flow_name, targetRowId, writeableProps); } else { - console.warn(`[SET ITEM AT INDEX] - No item at index ${targetIndex}`); + if (id) { + console.warn(`[SET ITEM] - No item with ID ${id}`); + } + if (_index !== undefined) { + console.warn(`[SET ITEM] - No item at index ${_index}`); + } } }, set_items: async ({ args, params }) => { From 41076119f9c396b423357560f39fe218dac3ed7e Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 6 Mar 2024 12:57:34 +0000 Subject: [PATCH 05/30] chore: remove workaround for fixed cdr issue; properly handle _index === 0 --- .../components/data-items/data-items.component.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/app/shared/components/template/components/data-items/data-items.component.ts b/src/app/shared/components/template/components/data-items/data-items.component.ts index 2cd9808690..f6edd40501 100644 --- a/src/app/shared/components/template/components/data-items/data-items.component.ts +++ b/src/app/shared/components/template/components/data-items/data-items.component.ts @@ -80,14 +80,6 @@ export class TmplDataItemsComponent extends TemplateBaseComponent implements OnD // TODO - deep diff and only update changed this.itemRows = replacedActionRows; this.cdr.markForCheck(); - // Without this second check, and without the delay, templates nested inside data-items loops do not render. - // There must be some async process somewhere but I'm not sure what it is - await new Promise((resolve) => { - setTimeout(() => { - resolve(); - }, 500); - }); - this.cdr.markForCheck(); } /** From 60d5ef0a3ed1bd5c32321d98dc5df4e969652b42 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 6 Mar 2024 12:57:58 +0000 Subject: [PATCH 06/30] chore: handle _index === 0 --- src/app/shared/services/dynamic-data/dynamic-data.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/services/dynamic-data/dynamic-data.service.ts b/src/app/shared/services/dynamic-data/dynamic-data.service.ts index 58a4fa09a8..2fdd9028c4 100644 --- a/src/app/shared/services/dynamic-data/dynamic-data.service.ts +++ b/src/app/shared/services/dynamic-data/dynamic-data.service.ts @@ -79,7 +79,7 @@ export class DynamicDataService extends AsyncServiceBase { // Target current row if another target is not explicitly provided let targetRowId = row_id; - if (_index) { + if (_index !== undefined) { targetRowId = row_ids[_index]; } if (id) { From 40e86f9b260091a198983a06ee86fdcff260ba30 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 6 Mar 2024 15:08:48 +0000 Subject: [PATCH 07/30] fix: @item references in expressions within data-items loops --- .../template/services/template-variables.service.ts | 5 +++++ src/app/shared/services/dynamic-data/dynamic-data.service.ts | 3 +++ 2 files changed, 8 insertions(+) diff --git a/src/app/shared/components/template/services/template-variables.service.ts b/src/app/shared/components/template/services/template-variables.service.ts index 90c41a5fe3..3beb437bef 100644 --- a/src/app/shared/components/template/services/template-variables.service.ts +++ b/src/app/shared/components/template/services/template-variables.service.ts @@ -183,6 +183,11 @@ export class TemplateVariablesService extends AsyncServiceBase { return evaluator.fullExpression.replace(/`/gi, ""); } + // Do not evaluate if the appropriate context is not available + if (type === "item" && !context.itemContext) { + return evaluator.fullExpression; + } + // process the main lookup, e.g. @local.some_val, @campaign.some_val // NOTE - if parse fail an empty string will be returned let { parsedValue, parseSuccess } = await this.processDynamicEvaluator(evaluator, context); diff --git a/src/app/shared/services/dynamic-data/dynamic-data.service.ts b/src/app/shared/services/dynamic-data/dynamic-data.service.ts index 2fdd9028c4..cf192c6687 100644 --- a/src/app/shared/services/dynamic-data/dynamic-data.service.ts +++ b/src/app/shared/services/dynamic-data/dynamic-data.service.ts @@ -87,6 +87,9 @@ export class DynamicDataService extends AsyncServiceBase { } if (row_ids.includes(targetRowId)) { + console.log( + `[SET ITEM] - Setting props on ${targetRowId}: ${JSON.stringify(writeableProps)}` + ); await this.update("data_list", flow_name, targetRowId, writeableProps); } else { if (id) { From e1ea106b35bc8f22c21d6e5daa86e34c35f8942c Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 6 Mar 2024 15:35:25 +0000 Subject: [PATCH 08/30] fix: complete merge of feat/data-items-eval-context --- .../components/template/processors/item.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/app/shared/components/template/processors/item.ts b/src/app/shared/components/template/processors/item.ts index 64990f119f..b72bdce542 100644 --- a/src/app/shared/components/template/processors/item.ts +++ b/src/app/shared/components/template/processors/item.ts @@ -36,9 +36,22 @@ export class ItemProcessor { */ private generateLoopItemRows(templateRows: FlowTypes.TemplateRow[], items: any[]) { const loopItemRows: FlowTypes.TemplateRow[] = []; - for (const [index, item] of Object.entries(items)) { - item._index = Number(index); - const evalContext = { itemContext: item }; + const lastItemIndex = items.length - 1; + for (const [indexKey, item] of Object.entries(items)) { + const _index = Number(indexKey); + const evalContext = { + itemContext: { + ...item, + // Assign row dynamic context to allow reference to rendered row metadata, including + // item index, id, and whether first or last item in list + _index, + _id: item["id"], + _first: _index === 0, + _last: _index === lastItemIndex, + }, + }; + console.log("first:", evalContext.itemContext._first); + console.log("last:", evalContext.itemContext._last); for (const r of templateRows) { const itemRow = this.setRecursiveRowEvalContext(r, evalContext); loopItemRows.push(itemRow); From 8b3b134e23ae282697c1c9bbc3802a00820efb25 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 6 Mar 2024 15:35:59 +0000 Subject: [PATCH 09/30] chore: remove comments --- src/app/shared/components/template/processors/item.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/shared/components/template/processors/item.ts b/src/app/shared/components/template/processors/item.ts index b72bdce542..b7d7f99b67 100644 --- a/src/app/shared/components/template/processors/item.ts +++ b/src/app/shared/components/template/processors/item.ts @@ -50,8 +50,6 @@ export class ItemProcessor { _last: _index === lastItemIndex, }, }; - console.log("first:", evalContext.itemContext._first); - console.log("last:", evalContext.itemContext._last); for (const r of templateRows) { const itemRow = this.setRecursiveRowEvalContext(r, evalContext); loopItemRows.push(itemRow); From 06cc641236d8b04b39770ef961757df96fdb7e87 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 6 Mar 2024 16:17:28 +0000 Subject: [PATCH 10/30] chore: code tidy --- .../dynamic-data/dynamic-data.service.ts | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/app/shared/services/dynamic-data/dynamic-data.service.ts b/src/app/shared/services/dynamic-data/dynamic-data.service.ts index cf192c6687..1db7159647 100644 --- a/src/app/shared/services/dynamic-data/dynamic-data.service.ts +++ b/src/app/shared/services/dynamic-data/dynamic-data.service.ts @@ -88,7 +88,7 @@ export class DynamicDataService extends AsyncServiceBase { if (row_ids.includes(targetRowId)) { console.log( - `[SET ITEM] - Setting props on ${targetRowId}: ${JSON.stringify(writeableProps)}` + `[SET ITEM] - Setting properties on ${targetRowId}: ${JSON.stringify(writeableProps)}` ); await this.update("data_list", flow_name, targetRowId, writeableProps); } else { @@ -257,18 +257,4 @@ export class DynamicDataService extends AsyncServiceBase { } return schema; } - - evaluateItemIndexString(targetIndexString: string, currentIndex: number) { - if (isNaN(Number(targetIndexString))) { - try { - // HACK use keyword "item_index" to refer to index of current item in loop - const jsExpression = targetIndexString.replace("item_index", currentIndex.toString()); - return evaluateJSExpression(jsExpression); - } catch { - console.error(`[SET ITEM AT INDEX] - Invalid index selector, "${targetIndexString}"`); - } - } else { - return Number(targetIndexString); - } - } } From 3ca52cad123fa674439e8cf18cf526dd5bd52d79 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 6 Mar 2024 18:10:15 +0000 Subject: [PATCH 11/30] chore: code tidy --- .../components/data-items/data-items.component.ts | 10 +++++----- .../services/dynamic-data/dynamic-data.service.ts | 14 +++++--------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/app/shared/components/template/components/data-items/data-items.component.ts b/src/app/shared/components/template/components/data-items/data-items.component.ts index 4f8758a441..2f482bae3e 100644 --- a/src/app/shared/components/template/components/data-items/data-items.component.ts +++ b/src/app/shared/components/template/components/data-items/data-items.component.ts @@ -96,8 +96,10 @@ export class TmplDataItemsComponent extends TemplateBaseComponent implements OnD const rowIds = Object.values(dataList).map((v) => v.id); const lastRowIndex = rowIds.length - 1; return rows.map((r) => { + const rowId = r._evalContext.itemContext.id; + // Reassign metadata fields previously assigned by item as rendered row count may have changed - const itemIndex = rowIds.indexOf(r._evalContext.itemContext.id); + const itemIndex = rowIds.indexOf(rowId); r._evalContext.itemContext = { ...r._evalContext.itemContext, _first: itemIndex === 0, @@ -108,14 +110,12 @@ export class TmplDataItemsComponent extends TemplateBaseComponent implements OnD if (r.action_list) { r.action_list = r.action_list.map((a) => { if (a.action_id === "set_item") { - const row_ids = Object.values(dataList).map((v) => v.id); - const row_id = r._evalContext.itemContext.id; - a.args = [this.dataListName, row_ids, row_id]; + a.args = [this.dataListName, rowIds, rowId]; } if (a.action_id === "set_items") { // TODO - add a check for @item refs and replace parameter list with correct values // for each individual item (default will be just to pick the first) - a.args = [this.dataListName, Object.values(dataList).map((v) => v.id)]; + a.args = [this.dataListName, rowIds]; } return a; }); diff --git a/src/app/shared/services/dynamic-data/dynamic-data.service.ts b/src/app/shared/services/dynamic-data/dynamic-data.service.ts index 1db7159647..4ffdf2b1d1 100644 --- a/src/app/shared/services/dynamic-data/dynamic-data.service.ts +++ b/src/app/shared/services/dynamic-data/dynamic-data.service.ts @@ -75,15 +75,15 @@ export class DynamicDataService extends AsyncServiceBase { */ set_item: async ({ args, params }) => { const [flow_name, row_ids, row_id] = args; - const { _index, id, ...writeableProps } = params; + const { _index, _id, ...writeableProps } = params; // Target current row if another target is not explicitly provided let targetRowId = row_id; if (_index !== undefined) { targetRowId = row_ids[_index]; } - if (id) { - targetRowId = id; + if (_id) { + targetRowId = _id; } if (row_ids.includes(targetRowId)) { @@ -92,8 +92,8 @@ export class DynamicDataService extends AsyncServiceBase { ); await this.update("data_list", flow_name, targetRowId, writeableProps); } else { - if (id) { - console.warn(`[SET ITEM] - No item with ID ${id}`); + if (_id) { + console.warn(`[SET ITEM] - No item with ID ${_id}`); } if (_index !== undefined) { console.warn(`[SET ITEM] - No item at index ${_index}`); @@ -107,10 +107,6 @@ export class DynamicDataService extends AsyncServiceBase { await this.update("data_list", flow_name, row_id, params); } }, - // "Reset" the data of a given data list by removing its dynamic user overwrites - reset_data: async ({ args }) => { - this.resetFlow("data_list", args[0]); - }, }); } From 989a7153ecb2f46ea2688f9dc25d777880178c11 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 3 Apr 2024 18:02:40 +0100 Subject: [PATCH 12/30] chore: post-merge fix --- packages/data-models/flowTypes.ts | 1 + .../components/data-items/data-items.component.ts | 2 +- .../template/services/template-variables.service.ts | 10 ++++++++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/data-models/flowTypes.ts b/packages/data-models/flowTypes.ts index 51148a9e69..328c4c7e46 100644 --- a/packages/data-models/flowTypes.ts +++ b/packages/data-models/flowTypes.ts @@ -352,6 +352,7 @@ export namespace FlowTypes { // item data [key: string]: any; } + export const itemMetadataFieldNames = ["_id", "_index", "_first", "_last"]; type IDynamicPrefix = IAppConfig["DYNAMIC_PREFIXES"][number]; diff --git a/src/app/shared/components/template/components/data-items/data-items.component.ts b/src/app/shared/components/template/components/data-items/data-items.component.ts index 7f94e2756f..5e72b6b33e 100644 --- a/src/app/shared/components/template/components/data-items/data-items.component.ts +++ b/src/app/shared/components/template/components/data-items/data-items.component.ts @@ -110,7 +110,7 @@ export class TmplDataItemsComponent extends TemplateBaseComponent implements OnD if (r.action_list) { r.action_list = r.action_list.map((a) => { if (a.action_id === "set_item") { - a.args = [this.dataListName, r._evalContext.itemContext._id]; + a.args = [this.dataListName, itemDataIDs, itemIndex]; } if (a.action_id === "set_items") { // TODO - add a check for @item refs and replace parameter list with correct values diff --git a/src/app/shared/components/template/services/template-variables.service.ts b/src/app/shared/components/template/services/template-variables.service.ts index 3beb437bef..ea3ee06bf6 100644 --- a/src/app/shared/components/template/services/template-variables.service.ts +++ b/src/app/shared/components/template/services/template-variables.service.ts @@ -124,9 +124,15 @@ export class TemplateVariablesService extends AsyncServiceBase { * Could provide single list of approved fields, but as dynamic fields also can be found in parameter lists * would likely prove too restrictive **/ - private shouldEvaluateField(fieldName: keyof FlowTypes.TemplateRow, omitFields: string[] = []) { + private shouldEvaluateField( + fieldName: keyof FlowTypes.TemplateRow | (typeof FlowTypes.itemMetadataFieldNames)[number], + omitFields: string[] = [] + ) { if (omitFields.includes(fieldName)) return false; - if (fieldName.startsWith("_") && fieldName !== "_index") return false; + // Explicitly approve fields that are names of item metadata fields, + // E.g. for use in actions such as `click | set_item | _index: @item._index + 1, completed:false` + if (FlowTypes.itemMetadataFieldNames.includes(fieldName)) return true; + if (fieldName.startsWith("_")) return false; return true; } From 5108803883670e1efaba48459ad03665c3a3dd8d Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Thu, 4 Apr 2024 10:22:31 +0100 Subject: [PATCH 13/30] chore: tidy comments --- .../template/services/template-variables.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/shared/components/template/services/template-variables.service.ts b/src/app/shared/components/template/services/template-variables.service.ts index ea3ee06bf6..dbe9e9f64c 100644 --- a/src/app/shared/components/template/services/template-variables.service.ts +++ b/src/app/shared/components/template/services/template-variables.service.ts @@ -120,7 +120,7 @@ export class TemplateVariablesService extends AsyncServiceBase { } /** - * Inore evaluation of meta, comment, and specifiedfields. + * Ignore evaluation of meta, comment, and specifiedfields. * Could provide single list of approved fields, but as dynamic fields also can be found in parameter lists * would likely prove too restrictive **/ @@ -129,7 +129,7 @@ export class TemplateVariablesService extends AsyncServiceBase { omitFields: string[] = [] ) { if (omitFields.includes(fieldName)) return false; - // Explicitly approve fields that are names of item metadata fields, + // Evaluate fields that are names of item metadata fields, e.g. "_index", "_id", // E.g. for use in actions such as `click | set_item | _index: @item._index + 1, completed:false` if (FlowTypes.itemMetadataFieldNames.includes(fieldName)) return true; if (fieldName.startsWith("_")) return false; From 5c21fd81ddcca70a50545f42f03ab0456ec93987 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 5 Apr 2024 17:30:18 +0100 Subject: [PATCH 14/30] chore: code tidy --- packages/data-models/flowTypes.ts | 1 - src/app/shared/services/dynamic-data/dynamic-data.service.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/data-models/flowTypes.ts b/packages/data-models/flowTypes.ts index 5feee5f085..a43a137606 100644 --- a/packages/data-models/flowTypes.ts +++ b/packages/data-models/flowTypes.ts @@ -406,7 +406,6 @@ export namespace FlowTypes { "pop_up", "process_template", "reset_app", - "reset_data", "save_to_device", "set_field", "set_item", diff --git a/src/app/shared/services/dynamic-data/dynamic-data.service.ts b/src/app/shared/services/dynamic-data/dynamic-data.service.ts index 4ffdf2b1d1..7d4ea0147e 100644 --- a/src/app/shared/services/dynamic-data/dynamic-data.service.ts +++ b/src/app/shared/services/dynamic-data/dynamic-data.service.ts @@ -70,7 +70,7 @@ export class DynamicDataService extends AsyncServiceBase { * Write properties on the current item (default), or on an explicitly targeted item, * e.g. * click | set_item | completed:true; - * click | set_item | id: @item.id, completed:true; + * click | set_item | _id: @item.id, completed:true; * click | set_item | _index: @item._index + 1, completed:true; */ set_item: async ({ args, params }) => { From fda82d6b9f4bc59580d96003afcdc2bed35c0f43 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 5 Apr 2024 18:10:35 +0100 Subject: [PATCH 15/30] fix: handle itemIndex passed from data items component --- .../services/dynamic-data/dynamic-data.service.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app/shared/services/dynamic-data/dynamic-data.service.ts b/src/app/shared/services/dynamic-data/dynamic-data.service.ts index 7d4ea0147e..ddad5f46fe 100644 --- a/src/app/shared/services/dynamic-data/dynamic-data.service.ts +++ b/src/app/shared/services/dynamic-data/dynamic-data.service.ts @@ -74,19 +74,22 @@ export class DynamicDataService extends AsyncServiceBase { * click | set_item | _index: @item._index + 1, completed:true; */ set_item: async ({ args, params }) => { - const [flow_name, row_ids, row_id] = args; + const [flow_name, itemDataIDs, itemIndex] = args; const { _index, _id, ...writeableProps } = params; + console.log("_index", _index); + console.log("_id", _id); + console.log("writeableProps", writeableProps); // Target current row if another target is not explicitly provided - let targetRowId = row_id; + let targetRowId = itemDataIDs[itemIndex]; if (_index !== undefined) { - targetRowId = row_ids[_index]; + targetRowId = itemDataIDs[_index]; } if (_id) { targetRowId = _id; } - if (row_ids.includes(targetRowId)) { + if (itemDataIDs.includes(targetRowId)) { console.log( `[SET ITEM] - Setting properties on ${targetRowId}: ${JSON.stringify(writeableProps)}` ); From f96209dfb509144f0c2a8ac2e8297535cd76df7c Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 5 Apr 2024 18:10:58 +0100 Subject: [PATCH 16/30] chore: remove debug logs --- src/app/shared/services/dynamic-data/dynamic-data.service.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/app/shared/services/dynamic-data/dynamic-data.service.ts b/src/app/shared/services/dynamic-data/dynamic-data.service.ts index ddad5f46fe..83fa718b07 100644 --- a/src/app/shared/services/dynamic-data/dynamic-data.service.ts +++ b/src/app/shared/services/dynamic-data/dynamic-data.service.ts @@ -77,9 +77,6 @@ export class DynamicDataService extends AsyncServiceBase { const [flow_name, itemDataIDs, itemIndex] = args; const { _index, _id, ...writeableProps } = params; - console.log("_index", _index); - console.log("_id", _id); - console.log("writeableProps", writeableProps); // Target current row if another target is not explicitly provided let targetRowId = itemDataIDs[itemIndex]; if (_index !== undefined) { From ef3047a6a0e032e16f4657bc564a1f7ed0e00d3e Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Fri, 12 Apr 2024 16:56:25 -0700 Subject: [PATCH 17/30] chore: review code tidying --- packages/data-models/flowTypes.ts | 13 +++++++++---- .../template/services/template-variables.service.ts | 7 +++++-- .../services/dynamic-data/dynamic-data.service.ts | 4 +++- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/data-models/flowTypes.ts b/packages/data-models/flowTypes.ts index a43a137606..9b568bfe78 100644 --- a/packages/data-models/flowTypes.ts +++ b/packages/data-models/flowTypes.ts @@ -343,16 +343,21 @@ export namespace FlowTypes { export type IDynamicField = { [key: string]: IDynamicField | TemplateRowDynamicEvaluator[] }; - export interface TemplateRowItemEvalContext { + export interface TemplateRowItemEvalContextMetadata { // item metadata _id: string; _index: number; _first: boolean; _last: boolean; - // item data - [key: string]: any; } - export const itemMetadataFieldNames = ["_id", "_index", "_first", "_last"]; + // Enumerable list of metadata columns for use by processing functions + export const TEMPLATE_ROW_ITEM_METADATA_FIELDS: Array = + ["_id", "_index", "_first", "_last"]; + + // General interface for row items which can contain any key-value pairs with metadata + export type TemplateRowItemEvalContext = TemplateRowItemEvalContextMetadata & { + [key: string]: any; + }; type IDynamicPrefix = IAppConfig["DYNAMIC_PREFIXES"][number]; diff --git a/src/app/shared/components/template/services/template-variables.service.ts b/src/app/shared/components/template/services/template-variables.service.ts index dbe9e9f64c..cfd818a7ec 100644 --- a/src/app/shared/components/template/services/template-variables.service.ts +++ b/src/app/shared/components/template/services/template-variables.service.ts @@ -16,6 +16,8 @@ const log = SHOW_DEBUG_LOGS ? console.log : () => null; const log_group = SHOW_DEBUG_LOGS ? console.group : () => null; const log_groupEnd = SHOW_DEBUG_LOGS ? console.groupEnd : () => null; +const { TEMPLATE_ROW_ITEM_METADATA_FIELDS } = FlowTypes; + /** * Most methods in this class depend on factors relating to the execution context * (e.g.row, variables etc.). Store as a single object to make it easier to pass between methods @@ -125,13 +127,14 @@ export class TemplateVariablesService extends AsyncServiceBase { * would likely prove too restrictive **/ private shouldEvaluateField( - fieldName: keyof FlowTypes.TemplateRow | (typeof FlowTypes.itemMetadataFieldNames)[number], + fieldName: keyof FlowTypes.TemplateRow | keyof FlowTypes.TemplateRowItemEvalContextMetadata, omitFields: string[] = [] ) { if (omitFields.includes(fieldName)) return false; + // Evaluate fields that are names of item metadata fields, e.g. "_index", "_id", // E.g. for use in actions such as `click | set_item | _index: @item._index + 1, completed:false` - if (FlowTypes.itemMetadataFieldNames.includes(fieldName)) return true; + if (TEMPLATE_ROW_ITEM_METADATA_FIELDS.includes(fieldName as any)) return true; if (fieldName.startsWith("_")) return false; return true; } diff --git a/src/app/shared/services/dynamic-data/dynamic-data.service.ts b/src/app/shared/services/dynamic-data/dynamic-data.service.ts index 83fa718b07..614994af30 100644 --- a/src/app/shared/services/dynamic-data/dynamic-data.service.ts +++ b/src/app/shared/services/dynamic-data/dynamic-data.service.ts @@ -6,7 +6,7 @@ import { FlowTypes } from "data-models"; import { environment } from "src/environments/environment"; import { AppDataService } from "../data/app-data.service"; import { AsyncServiceBase } from "../asyncService.base"; -import { arrayToHashmap, deepMergeObjects, evaluateJSExpression } from "../../utils"; +import { arrayToHashmap, deepMergeObjects } from "../../utils"; import { PersistedMemoryAdapter } from "./adapters/persistedMemory"; import { ReactiveMemoryAdapater, REACTIVE_SCHEMA_BASE } from "./adapters/reactiveMemory"; import { TemplateActionRegistry } from "../../components/template/services/instance/template-action.registry"; @@ -74,6 +74,8 @@ export class DynamicDataService extends AsyncServiceBase { * click | set_item | _index: @item._index + 1, completed:true; */ set_item: async ({ args, params }) => { + // data-items component populates data_list name, along with an array of all item ids + // and the index of the current item triggering the action const [flow_name, itemDataIDs, itemIndex] = args; const { _index, _id, ...writeableProps } = params; From e262c20ed5fbefd36b18578fab24eec6ee551e13 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Mon, 22 Apr 2024 10:50:19 +0100 Subject: [PATCH 18/30] chore: tidy console logs --- .../services/dynamic-data/dynamic-data.service.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/app/shared/services/dynamic-data/dynamic-data.service.ts b/src/app/shared/services/dynamic-data/dynamic-data.service.ts index 614994af30..20d8f588a2 100644 --- a/src/app/shared/services/dynamic-data/dynamic-data.service.ts +++ b/src/app/shared/services/dynamic-data/dynamic-data.service.ts @@ -89,17 +89,9 @@ export class DynamicDataService extends AsyncServiceBase { } if (itemDataIDs.includes(targetRowId)) { - console.log( - `[SET ITEM] - Setting properties on ${targetRowId}: ${JSON.stringify(writeableProps)}` - ); await this.update("data_list", flow_name, targetRowId, writeableProps); } else { - if (_id) { - console.warn(`[SET ITEM] - No item with ID ${_id}`); - } - if (_index !== undefined) { - console.warn(`[SET ITEM] - No item at index ${_index}`); - } + console.warn(`[SET ITEM] - No item ${_id ? "with ID " + _id : "at index " + _index}`); } }, set_items: async ({ args, params }) => { From 350bd473c2dee4673c633279125f555ce5382ef0 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Mon, 22 Apr 2024 12:19:17 +0100 Subject: [PATCH 19/30] chore: reset theme file --- src/theme/themes/pfr.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/theme/themes/pfr.scss b/src/theme/themes/pfr.scss index 6a63e928fe..37ceb53846 100644 --- a/src/theme/themes/pfr.scss +++ b/src/theme/themes/pfr.scss @@ -48,7 +48,7 @@ // radio-button-font-color: var(--ion-color-primary), ion-item-background: var(--ion-color-gray-light), // task-progress-bar-color: var(--ion-color-primary), - // checkbox-background-color: white,,,, + // checkbox-background-color: white,,, ); @include utils.generateTheme($color-primary, $color-secondary, $page-background, $g: $green); @each $name, $value in $variable-overrides { From 9f9f48313b03c7baa3106a09499c75a4a561ce30 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Mon, 22 Apr 2024 12:54:12 +0100 Subject: [PATCH 20/30] chore: tidy getTargetItemRow logic --- .../dynamic-data/dynamic-data.service.ts | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/app/shared/services/dynamic-data/dynamic-data.service.ts b/src/app/shared/services/dynamic-data/dynamic-data.service.ts index 20d8f588a2..afbf2ccbef 100644 --- a/src/app/shared/services/dynamic-data/dynamic-data.service.ts +++ b/src/app/shared/services/dynamic-data/dynamic-data.service.ts @@ -79,14 +79,7 @@ export class DynamicDataService extends AsyncServiceBase { const [flow_name, itemDataIDs, itemIndex] = args; const { _index, _id, ...writeableProps } = params; - // Target current row if another target is not explicitly provided - let targetRowId = itemDataIDs[itemIndex]; - if (_index !== undefined) { - targetRowId = itemDataIDs[_index]; - } - if (_id) { - targetRowId = _id; - } + const targetRowId = this.getTargetItemRowId(itemDataIDs, itemIndex, _id, _index); if (itemDataIDs.includes(targetRowId)) { await this.update("data_list", flow_name, targetRowId, writeableProps); @@ -247,4 +240,21 @@ export class DynamicDataService extends AsyncServiceBase { } return schema; } + + /** + * Get the ID of an item row to target for update: + * If a target _id is specified, this is returned, + * else if a target _index is specified, the corresponding item ID is returned, + * if neither is specified, then the ID of the current item is returned + */ + private getTargetItemRowId( + itemDataIDs: any[], + currentItemIndex: number, + _id: string, + _index: number + ) { + if (_id) return _id; + if (_index !== undefined) return itemDataIDs[_index]; + return itemDataIDs[currentItemIndex]; + } } From 1b1e782ff9be73a405abc4ed7bb4fe9e3bf9003c Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Mon, 22 Apr 2024 14:57:24 +0100 Subject: [PATCH 21/30] wip: create spec file for template variables service --- .../template-translate.service.spec.ts | 2 +- .../template-variables.service.spec.ts | 98 +++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 src/app/shared/components/template/services/template-variables.service.spec.ts diff --git a/src/app/shared/components/template/services/template-translate.service.spec.ts b/src/app/shared/components/template/services/template-translate.service.spec.ts index e293081bfa..249e87c97f 100644 --- a/src/app/shared/components/template/services/template-translate.service.spec.ts +++ b/src/app/shared/components/template/services/template-translate.service.spec.ts @@ -12,7 +12,7 @@ export class MockTemplateTranslateService implements Partial { +describe("TemplateTranslateService", () => { let service: TemplateTranslateService; beforeEach(() => { diff --git a/src/app/shared/components/template/services/template-variables.service.spec.ts b/src/app/shared/components/template/services/template-variables.service.spec.ts new file mode 100644 index 0000000000..59f969ce20 --- /dev/null +++ b/src/app/shared/components/template/services/template-variables.service.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from "@angular/core/testing"; +import { IVariableContext, TemplateVariablesService } from "./template-variables.service"; +import { FlowTypes } from "src/app/shared/model"; + +const EVALUATOR: FlowTypes.TemplateRowDynamicEvaluator = { + fullExpression: "text_completed_@item.id", + matchedExpression: "@item.id", + type: "item", + fieldName: "id", +}; + +const CONTEXT: IVariableContext = { + itemContext: { + id: "id_3", + label: "Task 3", + completed: true, + _index: 2, + _id: "id_3", + _first: false, + _last: true, + }, + templateRowMap: {}, + row: { + type: "text", + name: "text_completed_@item.id", + _nested_name: "data_items_4.text_completed_id_3", + }, + calcContext: { + globalConstants: { + test_var: "hello", + }, + globalFunctions: {}, + thisCtxt: { + app_day: 6, + app_first_launch: "2024-04-05T17:49:29", + fields: { + _app_language: "gb_en", + _app_skin: "default", + }, + local: { + button_list: [ + { + image: "images/icons/house_white.svg", + target_template: "home_screen", + }, + { + image: "images/icons/star_white.svg", + target_template: "comp_button", + }, + { + image: "images/icons/book_white.svg", + target_template: "comp_button", + }, + ], + }, + data: { + comp_data_items_list: { + id_1: { + id: "id_1", + label: "Task 1", + completed: false, + }, + id_2: { + id: "id_2", + label: "Task 2", + completed: true, + }, + id_3: { + id: "id_3", + label: "Task 3", + completed: true, + }, + }, + }, + item: { + id: "id_3", + completed: true, + _index: 0, + _id: "id_2", + _first: true, + _last: true, + }, + }, + }, +}; + +describe("TemplateVariablesService", () => { + let service: TemplateVariablesService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(TemplateVariablesService); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); +}); From b31500345095834542a726da02ea62b2bb5abf94 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 23 Apr 2024 11:35:41 +0100 Subject: [PATCH 22/30] wip: template variable spec test setup --- .../template-variables.service.spec.ts | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/src/app/shared/components/template/services/template-variables.service.spec.ts b/src/app/shared/components/template/services/template-variables.service.spec.ts index 59f969ce20..86bbd92b24 100644 --- a/src/app/shared/components/template/services/template-variables.service.spec.ts +++ b/src/app/shared/components/template/services/template-variables.service.spec.ts @@ -1,15 +1,21 @@ import { TestBed } from "@angular/core/testing"; import { IVariableContext, TemplateVariablesService } from "./template-variables.service"; import { FlowTypes } from "src/app/shared/model"; +import { HttpClientTestingModule } from "@angular/common/http/testing"; +import { TemplateFieldService } from "./template-field.service"; +import { MockTemplateFieldService } from "./template-field.service.spec"; +import { AppDataService } from "src/app/shared/services/data/app-data.service"; +import { CampaignService } from "src/app/feature/campaign/campaign.service"; +import { MockAppDataService } from "src/app/shared/services/data/app-data.service.spec"; -const EVALUATOR: FlowTypes.TemplateRowDynamicEvaluator = { +const MOCK_EVALUATOR: FlowTypes.TemplateRowDynamicEvaluator = { fullExpression: "text_completed_@item.id", matchedExpression: "@item.id", type: "item", fieldName: "id", }; -const CONTEXT: IVariableContext = { +const MOCK_CONTEXT: IVariableContext = { itemContext: { id: "id_3", label: "Task 3", @@ -84,11 +90,37 @@ const CONTEXT: IVariableContext = { }, }; +const MOCK_DATA = {}; + describe("TemplateVariablesService", () => { let service: TemplateVariablesService; + let getNextCampaignRowsSpy: jasmine.Spy; beforeEach(() => { - TestBed.configureTestingModule({}); + getNextCampaignRowsSpy = jasmine.createSpy(); + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: TemplateFieldService, + useValue: MockTemplateFieldService, + }, + { + provide: AppDataService, + useValue: new MockAppDataService(MOCK_DATA), + }, + // Mock single method from campaign service called + { + provide: CampaignService, + useValue: { + ready: async () => { + return true; + }, + getNextCampaignRows: getNextCampaignRowsSpy, + }, + }, + ], + }); service = TestBed.inject(TemplateVariablesService); }); From 72f89bb6956fc98ec525ad0f9d8aeeb77ac43a35 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 24 Apr 2024 14:12:10 +0100 Subject: [PATCH 23/30] test: add spec tests for template-variables service --- ....spec.ts => template-calc.service.spec.ts} | 16 ++ .../template-variables.service.spec.ts | 256 +++++++++++++++--- 2 files changed, 229 insertions(+), 43 deletions(-) rename src/app/shared/components/template/services/{template-calc.spec.ts => template-calc.service.spec.ts} (88%) diff --git a/src/app/shared/components/template/services/template-calc.spec.ts b/src/app/shared/components/template/services/template-calc.service.spec.ts similarity index 88% rename from src/app/shared/components/template/services/template-calc.spec.ts rename to src/app/shared/components/template/services/template-calc.service.spec.ts index 344a5affad..2752062482 100644 --- a/src/app/shared/components/template/services/template-calc.spec.ts +++ b/src/app/shared/components/template/services/template-calc.service.spec.ts @@ -1,3 +1,19 @@ +import { ICalcContext, TemplateCalcService } from "./template-calc.service"; + +export class MockTemplateCalcService implements Partial { + public async ready(): Promise { + return true; + } + + public getCalcContext(): ICalcContext { + return { + thisCtxt: {}, + globalConstants: {}, + globalFunctions: {}, + }; + } +} + /** * TODO - Add testing data and methods * diff --git a/src/app/shared/components/template/services/template-variables.service.spec.ts b/src/app/shared/components/template/services/template-variables.service.spec.ts index 86bbd92b24..32ebb0cd26 100644 --- a/src/app/shared/components/template/services/template-variables.service.spec.ts +++ b/src/app/shared/components/template/services/template-variables.service.spec.ts @@ -1,43 +1,109 @@ import { TestBed } from "@angular/core/testing"; import { IVariableContext, TemplateVariablesService } from "./template-variables.service"; -import { FlowTypes } from "src/app/shared/model"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { TemplateFieldService } from "./template-field.service"; import { MockTemplateFieldService } from "./template-field.service.spec"; import { AppDataService } from "src/app/shared/services/data/app-data.service"; import { CampaignService } from "src/app/feature/campaign/campaign.service"; import { MockAppDataService } from "src/app/shared/services/data/app-data.service.spec"; +import { TemplateCalcService } from "./template-calc.service"; +import { MockTemplateCalcService } from "./template-calc.service.spec"; -const MOCK_EVALUATOR: FlowTypes.TemplateRowDynamicEvaluator = { - fullExpression: "text_completed_@item.id", - matchedExpression: "@item.id", - type: "item", - fieldName: "id", -}; +const MOCK_APP_DATA = {}; + +const MOCK_ITEM_STRING = "@item._index + 1"; -const MOCK_CONTEXT: IVariableContext = { +// Context as logged from this debug template: +// https://docs.google.com/spreadsheets/d/1tL6CPHEIW-GPMYjdhVKQToy_hZ1H5qNIBkkh9XnA5QM/edit#gid=114708400 +const MOCK_CONTEXT_WITH_ITEM_CTXT: IVariableContext = { itemContext: { - id: "id_3", - label: "Task 3", - completed: true, - _index: 2, - _id: "id_3", - _first: false, - _last: true, + id: "id1", + number: 1, + string: "hello", + boolean: true, + _index: 0, + _id: "id1", + _first: true, + _last: false, + }, + templateRowMap: { + "data_items_2.text_1": { + type: "text", + value: 2, + _translations: { + value: {}, + }, + name: "text_1", + _nested_name: "data_items_2.text_1", + _dynamicFields: { + value: [ + { + fullExpression: "@item._index + 1", + matchedExpression: "@item._index", + type: "item", + fieldName: "_index", + }, + ], + }, + _dynamicDependencies: { + "@item._index": ["value"], + }, + _evalContext: { + itemContext: { + id: "id2", + number: 2, + string: "goodbye", + boolean: false, + _index: 1, + _id: "id2", + _first: false, + _last: true, + }, + }, + }, }, - templateRowMap: {}, row: { type: "text", - name: "text_completed_@item.id", - _nested_name: "data_items_4.text_completed_id_3", + value: "@item._index + 1", + _translations: { + value: {}, + }, + name: "text_1", + _nested_name: "data_items_2.text_1", + _dynamicFields: { + value: [ + { + fullExpression: "@item._index + 1", + matchedExpression: "@item._index", + type: "item", + fieldName: "_index", + }, + ], + }, + _dynamicDependencies: { + "@item._index": ["value"], + }, + _evalContext: { + itemContext: { + id: "id1", + number: 1, + string: "hello", + boolean: true, + _index: 0, + _id: "id1", + _first: true, + _last: false, + }, + }, }, + field: "value", calcContext: { globalConstants: { test_var: "hello", }, globalFunctions: {}, thisCtxt: { - app_day: 6, + app_day: 8, app_first_launch: "2024-04-05T17:49:29", fields: { _app_language: "gb_en", @@ -59,39 +125,130 @@ const MOCK_CONTEXT: IVariableContext = { }, ], }, - data: { - comp_data_items_list: { - id_1: { - id: "id_1", - label: "Task 1", - completed: false, + item: { + _index: 1, + }, + }, + }, +}; + +const MOCK_CONTEXT_WITHOUT_ITEM_CTXT: IVariableContext = { + templateRowMap: { + "data_items_2.text_1": { + type: "text", + value: "@item._index + 1", + _translations: { + value: {}, + }, + name: "text_1", + _nested_name: "data_items_2.text_1", + _dynamicFields: { + value: [ + { + fullExpression: "@item._index + 1", + matchedExpression: "@item._index", + type: "item", + fieldName: "_index", + }, + ], + }, + _dynamicDependencies: { + "@item._index": ["value"], + }, + }, + data_items_2: { + type: "data_items", + value: "debug_item_data", + rows: [ + { + type: "text", + value: "@item._index + 1", + _translations: { + value: {}, }, - id_2: { - id: "id_2", - label: "Task 2", - completed: true, + name: "text_1", + _nested_name: "data_items_2.text_1", + _dynamicFields: { + value: [ + { + fullExpression: "@item._index + 1", + matchedExpression: "@item._index", + type: "item", + fieldName: "_index", + }, + ], }, - id_3: { - id: "id_3", - label: "Task 3", - completed: true, + _dynamicDependencies: { + "@item._index": ["value"], }, }, + ], + name: "data_items_2", + _nested_name: "data_items_2", + }, + }, + row: { + type: "text", + value: "@item._index + 1", + _translations: { + value: {}, + }, + name: "text_1", + _nested_name: "data_items_2.text_1", + _dynamicFields: { + value: [ + { + fullExpression: "@item._index + 1", + matchedExpression: "@item._index", + type: "item", + fieldName: "_index", + }, + ], + }, + _dynamicDependencies: { + "@item._index": ["value"], + }, + }, + field: "value", + calcContext: { + globalConstants: { + test_var: "hello", + }, + globalFunctions: {}, + thisCtxt: { + app_day: 8, + app_first_launch: "2024-04-05T17:49:29", + fields: { + _app_language: "gb_en", + _app_skin: "default", + }, + local: { + button_list: [ + { + image: "images/icons/house_white.svg", + target_template: "home_screen", + }, + { + image: "images/icons/star_white.svg", + target_template: "comp_button", + }, + { + image: "images/icons/book_white.svg", + target_template: "comp_button", + }, + ], }, item: { - id: "id_3", - completed: true, - _index: 0, - _id: "id_2", - _first: true, - _last: true, + _index: 1, }, }, }, }; -const MOCK_DATA = {}; - +/** + * Call standalone tests via: + * yarn ng test --include src/app/shared/components/template/services/template-variables.service.spec.ts + */ describe("TemplateVariablesService", () => { let service: TemplateVariablesService; let getNextCampaignRowsSpy: jasmine.Spy; @@ -103,11 +260,15 @@ describe("TemplateVariablesService", () => { providers: [ { provide: TemplateFieldService, - useValue: MockTemplateFieldService, + useValue: new MockTemplateFieldService(), }, { provide: AppDataService, - useValue: new MockAppDataService(MOCK_DATA), + useValue: new MockAppDataService(MOCK_APP_DATA), + }, + { + provide: TemplateCalcService, + useValue: new MockTemplateCalcService(), }, // Mock single method from campaign service called { @@ -127,4 +288,13 @@ describe("TemplateVariablesService", () => { it("should be created", () => { expect(service).toBeTruthy(); }); + + it("evaluates string containing item variable", async () => { + const value = await service.evaluatePLHData(MOCK_ITEM_STRING, MOCK_CONTEXT_WITH_ITEM_CTXT); + expect(value).toBe(1); + }); + it("does not evaluate item string without appropriate context", async () => { + const value = await service.evaluatePLHData(MOCK_ITEM_STRING, MOCK_CONTEXT_WITHOUT_ITEM_CTXT); + expect(value).toBe(MOCK_ITEM_STRING); + }); }); From 9ea2aebb1b5477171acc7ffe29c2ce5ec6865b77 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 24 Apr 2024 14:14:23 +0100 Subject: [PATCH 24/30] refactor: set_item logic --- .../data-items/data-items.component.ts | 16 ++++- .../dynamic-data/dynamic-data.service.ts | 71 ++++++++++++------- 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/src/app/shared/components/template/components/data-items/data-items.component.ts b/src/app/shared/components/template/components/data-items/data-items.component.ts index 5e72b6b33e..116831e93d 100644 --- a/src/app/shared/components/template/components/data-items/data-items.component.ts +++ b/src/app/shared/components/template/components/data-items/data-items.component.ts @@ -7,7 +7,10 @@ import { OnDestroy, } from "@angular/core"; import { debounceTime, Subscription } from "rxjs"; -import { DynamicDataService } from "src/app/shared/services/dynamic-data/dynamic-data.service"; +import { + DynamicDataService, + ISetItemContext, +} from "src/app/shared/services/dynamic-data/dynamic-data.service"; import { FlowTypes } from "../../models"; import { ItemProcessor } from "../../processors/item"; import { TemplateRowService } from "../../services/instance/template-row.service"; @@ -96,8 +99,9 @@ export class TmplDataItemsComponent extends TemplateBaseComponent implements OnD const itemDataIDs = itemData.map((item) => item.id); // Reassign metadata fields previously assigned by item as rendered row count may have changed return templateRows.map((r) => { + const itemId = r._evalContext.itemContext._id; // Map the row item context to the original list of items rendered to know position in item list. - const itemIndex = itemDataIDs.indexOf(r._evalContext.itemContext._id); + const itemIndex = itemDataIDs.indexOf(itemId); // Update metadata fields as _first, _last and index may have changed based on dynamic updates r._evalContext.itemContext = { ...r._evalContext.itemContext, @@ -110,7 +114,13 @@ export class TmplDataItemsComponent extends TemplateBaseComponent implements OnD if (r.action_list) { r.action_list = r.action_list.map((a) => { if (a.action_id === "set_item") { - a.args = [this.dataListName, itemDataIDs, itemIndex]; + // a.args = [this.dataListName, itemDataIDs, itemId]; + const setItemContext: ISetItemContext = { + flow_name: this.dataListName, + itemDataIDs, + currentItemId: itemId, + }; + a.args = [setItemContext]; } if (a.action_id === "set_items") { // TODO - add a check for @item refs and replace parameter list with correct values diff --git a/src/app/shared/services/dynamic-data/dynamic-data.service.ts b/src/app/shared/services/dynamic-data/dynamic-data.service.ts index afbf2ccbef..d54b2b5c48 100644 --- a/src/app/shared/services/dynamic-data/dynamic-data.service.ts +++ b/src/app/shared/services/dynamic-data/dynamic-data.service.ts @@ -74,23 +74,16 @@ export class DynamicDataService extends AsyncServiceBase { * click | set_item | _index: @item._index + 1, completed:true; */ set_item: async ({ args, params }) => { - // data-items component populates data_list name, along with an array of all item ids - // and the index of the current item triggering the action - const [flow_name, itemDataIDs, itemIndex] = args; + // The data-items component populates the context for evaluating the target item to be updated + const context = args[0] as ISetItemContext; + // The params come directly from the authored action const { _index, _id, ...writeableProps } = params; - - const targetRowId = this.getTargetItemRowId(itemDataIDs, itemIndex, _id, _index); - - if (itemDataIDs.includes(targetRowId)) { - await this.update("data_list", flow_name, targetRowId, writeableProps); - } else { - console.warn(`[SET ITEM] - No item ${_id ? "with ID " + _id : "at index " + _index}`); - } + await this.setItem({ context, _index, _id, writeableProps }); }, set_items: async ({ args, params }) => { - const [flow_name, row_ids] = args; + const { flow_name, itemDataIDs } = args[0] as ISetItemContext; // Hack, no current method for bulk update so make successive (changes debounced in component) - for (const row_id of row_ids) { + for (const row_id of itemDataIDs) { await this.update("data_list", flow_name, row_id, params); } }, @@ -242,19 +235,45 @@ export class DynamicDataService extends AsyncServiceBase { } /** - * Get the ID of an item row to target for update: - * If a target _id is specified, this is returned, - * else if a target _index is specified, the corresponding item ID is returned, - * if neither is specified, then the ID of the current item is returned + * Update an "item", a row within a data-items loop, e.g. as triggered by the set_item action. + * If an _id is specified, the row with that ID is updated, + * else if an _index is specified, the row with corresponding to the item at that index is updated, + * if neither is specified, then the current item (as defined in context) is updated */ - private getTargetItemRowId( - itemDataIDs: any[], - currentItemIndex: number, - _id: string, - _index: number - ) { - if (_id) return _id; - if (_index !== undefined) return itemDataIDs[_index]; - return itemDataIDs[currentItemIndex]; + public async setItem(params: { + context: ISetItemContext; + /** the index of a specific item to update */ + _index?: number; + /** the id of a specific item to update */ + _id?: string; + /** the property key/values to update on the targeted item */ + writeableProps: any; + }) { + const { flow_name, itemDataIDs, currentItemId } = params.context; + const { _index, _id, writeableProps } = params; + + let targetRowId = currentItemId; + if (_id) { + targetRowId = _id; + } + if (_index !== undefined) { + targetRowId = itemDataIDs[_index]; + } + + if (itemDataIDs.includes(targetRowId)) { + await this.update("data_list", flow_name, targetRowId, writeableProps); + } else { + console.warn(`[SET ITEM] - No item ${_id ? "with ID " + _id : "at index " + _index}`); + } } } + +/** the context for evaluating the target item to be updated, provided by the data-items component */ +export interface ISetItemContext { + /** the name of the data_list containing the item to update */ + flow_name: string; + /** an array of the IDs of all the item rows in the loop */ + itemDataIDs: string[]; + /** the ID of the current item */ + currentItemId: string; +} From c327d3a4fc5750c61290c1d1828e92350a94b50b Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 24 Apr 2024 14:14:55 +0100 Subject: [PATCH 25/30] test: add spec tests for dynamic data setItem logic --- .../dynamic-data/dynamic-data.service.spec.ts | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/app/shared/services/dynamic-data/dynamic-data.service.spec.ts b/src/app/shared/services/dynamic-data/dynamic-data.service.spec.ts index b9e66c86b0..50ec015cc2 100644 --- a/src/app/shared/services/dynamic-data/dynamic-data.service.spec.ts +++ b/src/app/shared/services/dynamic-data/dynamic-data.service.spec.ts @@ -2,7 +2,7 @@ import { TestBed } from "@angular/core/testing"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { firstValueFrom } from "rxjs"; -import { DynamicDataService } from "./dynamic-data.service"; +import { DynamicDataService, ISetItemContext } from "./dynamic-data.service"; import { AppDataService } from "../data/app-data.service"; import { MockAppDataService } from "../data/app-data.service.spec"; @@ -12,6 +12,12 @@ const TEST_DATA_ROWS = [ ]; type ITestRow = (typeof TEST_DATA_ROWS)[number]; +const SET_ITEM_CONTEXT: ISetItemContext = { + flow_name: "test_flow", + itemDataIDs: ["id1", "id2"], + currentItemId: "id1", +}; + /** * Call standalone tests via: * yarn ng test --include src/app/shared/services/dynamic-data/dynamic-data.service.spec.ts @@ -102,6 +108,41 @@ describe("DynamicDataService", () => { expect(res.length).toEqual(20); }); + it("sets an item correctly for current item", async () => { + await service.setItem({ + context: SET_ITEM_CONTEXT, + writeableProps: { string: "sets an item correctly for current item" }, + }); + const obs = await service.query$("data_list", "test_flow"); + const data = await firstValueFrom(obs); + expect(data[0].string).toEqual("sets an item correctly for current item"); + expect(data[1].string).toEqual("goodbye"); + }); + + it("sets an item correctly for a given _id", async () => { + await service.setItem({ + context: SET_ITEM_CONTEXT, + _id: "id2", + writeableProps: { string: "sets an item correctly for a given _id" }, + }); + const obs = await service.query$("data_list", "test_flow"); + const data = await firstValueFrom(obs); + expect(data[0].string).toEqual("hello"); + expect(data[1].string).toEqual("sets an item correctly for a given _id"); + }); + + it("sets an item correctly for a given _index", async () => { + await service.setItem({ + context: SET_ITEM_CONTEXT, + _index: 1, + writeableProps: { string: "sets an item correctly for a given _index" }, + }); + const obs = await service.query$("data_list", "test_flow"); + const data = await firstValueFrom(obs); + expect(data[0].string).toEqual("hello"); + expect(data[1].string).toEqual("sets an item correctly for a given _index"); + }); + // QA it("prevents query of non-existent data lists", async () => { let errMsg: string; From 8a95b0d4fa8341ef0d20dc29bf26a106844cdc94 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 24 Apr 2024 14:21:46 +0100 Subject: [PATCH 26/30] refactor: set_item logic --- .../data-items/data-items.component.ts | 17 +++-- .../dynamic-data/dynamic-data.service.ts | 71 ++++++++++++------- 2 files changed, 58 insertions(+), 30 deletions(-) diff --git a/src/app/shared/components/template/components/data-items/data-items.component.ts b/src/app/shared/components/template/components/data-items/data-items.component.ts index 5e72b6b33e..b32c9275ee 100644 --- a/src/app/shared/components/template/components/data-items/data-items.component.ts +++ b/src/app/shared/components/template/components/data-items/data-items.component.ts @@ -7,7 +7,10 @@ import { OnDestroy, } from "@angular/core"; import { debounceTime, Subscription } from "rxjs"; -import { DynamicDataService } from "src/app/shared/services/dynamic-data/dynamic-data.service"; +import { + DynamicDataService, + ISetItemContext, +} from "src/app/shared/services/dynamic-data/dynamic-data.service"; import { FlowTypes } from "../../models"; import { ItemProcessor } from "../../processors/item"; import { TemplateRowService } from "../../services/instance/template-row.service"; @@ -96,8 +99,9 @@ export class TmplDataItemsComponent extends TemplateBaseComponent implements OnD const itemDataIDs = itemData.map((item) => item.id); // Reassign metadata fields previously assigned by item as rendered row count may have changed return templateRows.map((r) => { + const itemId = r._evalContext.itemContext._id; // Map the row item context to the original list of items rendered to know position in item list. - const itemIndex = itemDataIDs.indexOf(r._evalContext.itemContext._id); + const itemIndex = itemDataIDs.indexOf(itemId); // Update metadata fields as _first, _last and index may have changed based on dynamic updates r._evalContext.itemContext = { ...r._evalContext.itemContext, @@ -108,14 +112,19 @@ export class TmplDataItemsComponent extends TemplateBaseComponent implements OnD // Update any action list set_item args to contain name of current data list and item id // and set_items action to include all currently displayed rows if (r.action_list) { + const setItemContext: ISetItemContext = { + flow_name: this.dataListName, + itemDataIDs, + currentItemId: itemId, + }; r.action_list = r.action_list.map((a) => { if (a.action_id === "set_item") { - a.args = [this.dataListName, itemDataIDs, itemIndex]; + a.args = [setItemContext]; } if (a.action_id === "set_items") { // TODO - add a check for @item refs and replace parameter list with correct values // for each individual item (default will be just to pick the first) - a.args = [this.dataListName, itemDataIDs]; + a.args = [setItemContext]; } return a; }); diff --git a/src/app/shared/services/dynamic-data/dynamic-data.service.ts b/src/app/shared/services/dynamic-data/dynamic-data.service.ts index afbf2ccbef..d54b2b5c48 100644 --- a/src/app/shared/services/dynamic-data/dynamic-data.service.ts +++ b/src/app/shared/services/dynamic-data/dynamic-data.service.ts @@ -74,23 +74,16 @@ export class DynamicDataService extends AsyncServiceBase { * click | set_item | _index: @item._index + 1, completed:true; */ set_item: async ({ args, params }) => { - // data-items component populates data_list name, along with an array of all item ids - // and the index of the current item triggering the action - const [flow_name, itemDataIDs, itemIndex] = args; + // The data-items component populates the context for evaluating the target item to be updated + const context = args[0] as ISetItemContext; + // The params come directly from the authored action const { _index, _id, ...writeableProps } = params; - - const targetRowId = this.getTargetItemRowId(itemDataIDs, itemIndex, _id, _index); - - if (itemDataIDs.includes(targetRowId)) { - await this.update("data_list", flow_name, targetRowId, writeableProps); - } else { - console.warn(`[SET ITEM] - No item ${_id ? "with ID " + _id : "at index " + _index}`); - } + await this.setItem({ context, _index, _id, writeableProps }); }, set_items: async ({ args, params }) => { - const [flow_name, row_ids] = args; + const { flow_name, itemDataIDs } = args[0] as ISetItemContext; // Hack, no current method for bulk update so make successive (changes debounced in component) - for (const row_id of row_ids) { + for (const row_id of itemDataIDs) { await this.update("data_list", flow_name, row_id, params); } }, @@ -242,19 +235,45 @@ export class DynamicDataService extends AsyncServiceBase { } /** - * Get the ID of an item row to target for update: - * If a target _id is specified, this is returned, - * else if a target _index is specified, the corresponding item ID is returned, - * if neither is specified, then the ID of the current item is returned + * Update an "item", a row within a data-items loop, e.g. as triggered by the set_item action. + * If an _id is specified, the row with that ID is updated, + * else if an _index is specified, the row with corresponding to the item at that index is updated, + * if neither is specified, then the current item (as defined in context) is updated */ - private getTargetItemRowId( - itemDataIDs: any[], - currentItemIndex: number, - _id: string, - _index: number - ) { - if (_id) return _id; - if (_index !== undefined) return itemDataIDs[_index]; - return itemDataIDs[currentItemIndex]; + public async setItem(params: { + context: ISetItemContext; + /** the index of a specific item to update */ + _index?: number; + /** the id of a specific item to update */ + _id?: string; + /** the property key/values to update on the targeted item */ + writeableProps: any; + }) { + const { flow_name, itemDataIDs, currentItemId } = params.context; + const { _index, _id, writeableProps } = params; + + let targetRowId = currentItemId; + if (_id) { + targetRowId = _id; + } + if (_index !== undefined) { + targetRowId = itemDataIDs[_index]; + } + + if (itemDataIDs.includes(targetRowId)) { + await this.update("data_list", flow_name, targetRowId, writeableProps); + } else { + console.warn(`[SET ITEM] - No item ${_id ? "with ID " + _id : "at index " + _index}`); + } } } + +/** the context for evaluating the target item to be updated, provided by the data-items component */ +export interface ISetItemContext { + /** the name of the data_list containing the item to update */ + flow_name: string; + /** an array of the IDs of all the item rows in the loop */ + itemDataIDs: string[]; + /** the ID of the current item */ + currentItemId: string; +} From 2a2f1e51ae507f39e226ec8143e18c022bfce8f0 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 24 Apr 2024 14:22:03 +0100 Subject: [PATCH 27/30] test: add spec tests for dynamic data setItem logic --- .../dynamic-data/dynamic-data.service.spec.ts | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/app/shared/services/dynamic-data/dynamic-data.service.spec.ts b/src/app/shared/services/dynamic-data/dynamic-data.service.spec.ts index b9e66c86b0..50ec015cc2 100644 --- a/src/app/shared/services/dynamic-data/dynamic-data.service.spec.ts +++ b/src/app/shared/services/dynamic-data/dynamic-data.service.spec.ts @@ -2,7 +2,7 @@ import { TestBed } from "@angular/core/testing"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { firstValueFrom } from "rxjs"; -import { DynamicDataService } from "./dynamic-data.service"; +import { DynamicDataService, ISetItemContext } from "./dynamic-data.service"; import { AppDataService } from "../data/app-data.service"; import { MockAppDataService } from "../data/app-data.service.spec"; @@ -12,6 +12,12 @@ const TEST_DATA_ROWS = [ ]; type ITestRow = (typeof TEST_DATA_ROWS)[number]; +const SET_ITEM_CONTEXT: ISetItemContext = { + flow_name: "test_flow", + itemDataIDs: ["id1", "id2"], + currentItemId: "id1", +}; + /** * Call standalone tests via: * yarn ng test --include src/app/shared/services/dynamic-data/dynamic-data.service.spec.ts @@ -102,6 +108,41 @@ describe("DynamicDataService", () => { expect(res.length).toEqual(20); }); + it("sets an item correctly for current item", async () => { + await service.setItem({ + context: SET_ITEM_CONTEXT, + writeableProps: { string: "sets an item correctly for current item" }, + }); + const obs = await service.query$("data_list", "test_flow"); + const data = await firstValueFrom(obs); + expect(data[0].string).toEqual("sets an item correctly for current item"); + expect(data[1].string).toEqual("goodbye"); + }); + + it("sets an item correctly for a given _id", async () => { + await service.setItem({ + context: SET_ITEM_CONTEXT, + _id: "id2", + writeableProps: { string: "sets an item correctly for a given _id" }, + }); + const obs = await service.query$("data_list", "test_flow"); + const data = await firstValueFrom(obs); + expect(data[0].string).toEqual("hello"); + expect(data[1].string).toEqual("sets an item correctly for a given _id"); + }); + + it("sets an item correctly for a given _index", async () => { + await service.setItem({ + context: SET_ITEM_CONTEXT, + _index: 1, + writeableProps: { string: "sets an item correctly for a given _index" }, + }); + const obs = await service.query$("data_list", "test_flow"); + const data = await firstValueFrom(obs); + expect(data[0].string).toEqual("hello"); + expect(data[1].string).toEqual("sets an item correctly for a given _index"); + }); + // QA it("prevents query of non-existent data lists", async () => { let errMsg: string; From 378f2562d8771839eac83f45fe919463c56a7283 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 29 Apr 2024 13:02:30 -0700 Subject: [PATCH 28/30] chore: mock template-field and error-handler service bootstrap --- .../template/services/template-field.service.spec.ts | 6 +++++- .../error-handler/error-handler.service.spec.ts | 10 +++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/app/shared/components/template/services/template-field.service.spec.ts b/src/app/shared/components/template/services/template-field.service.spec.ts index f2ae3e1291..dcfd73f763 100644 --- a/src/app/shared/components/template/services/template-field.service.spec.ts +++ b/src/app/shared/components/template/services/template-field.service.spec.ts @@ -3,6 +3,8 @@ import { TestBed } from "@angular/core/testing"; import { TemplateFieldService } from "./template-field.service"; import type { PromiseExtended } from "dexie"; import { booleanStringToBoolean } from "src/app/shared/utils"; +import { ErrorHandlerService } from "src/app/shared/services/error-handler/error-handler.service"; +import { MockErrorHandlerService } from "src/app/shared/services/error-handler/error-handler.service.spec"; /** Mock calls for field values from the template field service to return test data */ export class MockTemplateFieldService implements Partial { @@ -27,11 +29,13 @@ export class MockTemplateFieldService implements Partial { describe("TemplateFieldService", () => { let service: TemplateFieldService; - beforeEach(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], + providers: [{ provide: ErrorHandlerService, useValue: new MockErrorHandlerService() }], }); service = TestBed.inject(TemplateFieldService); + await service.ready(); }); it("should be created", () => { diff --git a/src/app/shared/services/error-handler/error-handler.service.spec.ts b/src/app/shared/services/error-handler/error-handler.service.spec.ts index 943381c9f6..96afdbf7f1 100644 --- a/src/app/shared/services/error-handler/error-handler.service.spec.ts +++ b/src/app/shared/services/error-handler/error-handler.service.spec.ts @@ -1,6 +1,7 @@ import { TestBed } from "@angular/core/testing"; import { ErrorHandlerService } from "./error-handler.service"; +import { FirebaseService } from "../firebase/firebase.service"; /** Mock calls for sheets from the appData service to return test data */ export class MockErrorHandlerService implements Partial { @@ -16,7 +17,14 @@ describe("ErrorHandlerService", () => { let service: ErrorHandlerService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + providers: [ + { + provide: FirebaseService, + useValue: {}, + }, + ], + }); service = TestBed.inject(ErrorHandlerService); }); From 149c868ab3fe44cd163bcfc840f8e9e70e0997cd Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 29 Apr 2024 22:38:37 -0700 Subject: [PATCH 29/30] refactor: template-variable specs Remove unnecessary hardcoded data and add additional public method tests --- .../template-variables.service.spec.ts | 301 ++++++------------ 1 file changed, 91 insertions(+), 210 deletions(-) diff --git a/src/app/shared/components/template/services/template-variables.service.spec.ts b/src/app/shared/components/template/services/template-variables.service.spec.ts index 32ebb0cd26..fbacc2c688 100644 --- a/src/app/shared/components/template/services/template-variables.service.spec.ts +++ b/src/app/shared/components/template/services/template-variables.service.spec.ts @@ -11,190 +11,60 @@ import { MockTemplateCalcService } from "./template-calc.service.spec"; const MOCK_APP_DATA = {}; -const MOCK_ITEM_STRING = "@item._index + 1"; +// Fields populated to mock field service +const MOCK_FIELDS = { + _app_language: "gb_en", + _app_skin: "default", + string_field: "test_string_value", + number_field: 2, +}; -// Context as logged from this debug template: -// https://docs.google.com/spreadsheets/d/1tL6CPHEIW-GPMYjdhVKQToy_hZ1H5qNIBkkh9XnA5QM/edit#gid=114708400 -const MOCK_CONTEXT_WITH_ITEM_CTXT: IVariableContext = { - itemContext: { - id: "id1", - number: 1, - string: "hello", - boolean: true, - _index: 0, - _id: "id1", - _first: true, - _last: false, - }, - templateRowMap: { - "data_items_2.text_1": { - type: "text", - value: 2, - _translations: { - value: {}, - }, - name: "text_1", - _nested_name: "data_items_2.text_1", - _dynamicFields: { - value: [ - { - fullExpression: "@item._index + 1", - matchedExpression: "@item._index", - type: "item", - fieldName: "_index", - }, - ], - }, - _dynamicDependencies: { - "@item._index": ["value"], - }, - _evalContext: { - itemContext: { - id: "id2", - number: 2, - string: "goodbye", - boolean: false, - _index: 1, - _id: "id2", - _first: false, - _last: true, - }, - }, - }, - }, +const MOCK_CONTEXT_BASE: IVariableContext = { + // Assume the row will have a dynamic 'field' entry + field: "value", row: { type: "text", - value: "@item._index + 1", - _translations: { - value: {}, - }, - name: "text_1", - _nested_name: "data_items_2.text_1", - _dynamicFields: { - value: [ - { - fullExpression: "@item._index + 1", - matchedExpression: "@item._index", - type: "item", - fieldName: "_index", - }, - ], - }, - _dynamicDependencies: { - "@item._index": ["value"], - }, - _evalContext: { - itemContext: { - id: "id1", - number: 1, - string: "hello", - boolean: true, - _index: 0, - _id: "id1", - _first: true, - _last: false, - }, - }, + value: "", + name: "test_row", + _nested_name: "test_row", }, - field: "value", + templateRowMap: {}, calcContext: { - globalConstants: { - test_var: "hello", - }, + globalConstants: {}, globalFunctions: {}, thisCtxt: { - app_day: 8, - app_first_launch: "2024-04-05T17:49:29", - fields: { - _app_language: "gb_en", - _app_skin: "default", - }, - local: { - button_list: [ - { - image: "images/icons/house_white.svg", - target_template: "home_screen", - }, - { - image: "images/icons/star_white.svg", - target_template: "comp_button", - }, - { - image: "images/icons/book_white.svg", - target_template: "comp_button", - }, - ], - }, - item: { - _index: 1, - }, + fields: MOCK_FIELDS, + local: {}, }, }, }; -const MOCK_CONTEXT_WITHOUT_ITEM_CTXT: IVariableContext = { - templateRowMap: { - "data_items_2.text_1": { - type: "text", - value: "@item._index + 1", - _translations: { - value: {}, - }, - name: "text_1", - _nested_name: "data_items_2.text_1", - _dynamicFields: { - value: [ - { - fullExpression: "@item._index + 1", - matchedExpression: "@item._index", - type: "item", - fieldName: "_index", - }, - ], - }, - _dynamicDependencies: { - "@item._index": ["value"], - }, - }, - data_items_2: { - type: "data_items", - value: "debug_item_data", - rows: [ +const TEST_FIELD_CONTEXT: IVariableContext = { + ...MOCK_CONTEXT_BASE, + row: { + ...MOCK_CONTEXT_BASE.row, + value: "Hello @fields.string_field", + _dynamicFields: { + value: [ { - type: "text", - value: "@item._index + 1", - _translations: { - value: {}, - }, - name: "text_1", - _nested_name: "data_items_2.text_1", - _dynamicFields: { - value: [ - { - fullExpression: "@item._index + 1", - matchedExpression: "@item._index", - type: "item", - fieldName: "_index", - }, - ], - }, - _dynamicDependencies: { - "@item._index": ["value"], - }, + fullExpression: "Hello @fields.string_field", + matchedExpression: "@fields.string_field", + type: "fields", + fieldName: "string_field", }, ], - name: "data_items_2", - _nested_name: "data_items_2", }, }, +}; + +// Context adapted from this debug template: +// https://docs.google.com/spreadsheets/d/1tL6CPHEIW-GPMYjdhVKQToy_hZ1H5qNIBkkh9XnA5QM/edit#gid=114708400 +const TEST_ITEM_CONTEXT: IVariableContext = { + ...MOCK_CONTEXT_BASE, row: { - type: "text", + ...MOCK_CONTEXT_BASE.row, value: "@item._index + 1", - _translations: { - value: {}, - }, - name: "text_1", - _nested_name: "data_items_2.text_1", + // NOTE - any evaluated fields should appea _dynamicFields: { value: [ { @@ -205,43 +75,16 @@ const MOCK_CONTEXT_WITHOUT_ITEM_CTXT: IVariableContext = { }, ], }, - _dynamicDependencies: { - "@item._index": ["value"], - }, }, - field: "value", - calcContext: { - globalConstants: { - test_var: "hello", - }, - globalFunctions: {}, - thisCtxt: { - app_day: 8, - app_first_launch: "2024-04-05T17:49:29", - fields: { - _app_language: "gb_en", - _app_skin: "default", - }, - local: { - button_list: [ - { - image: "images/icons/house_white.svg", - target_template: "home_screen", - }, - { - image: "images/icons/star_white.svg", - target_template: "comp_button", - }, - { - image: "images/icons/book_white.svg", - target_template: "comp_button", - }, - ], - }, - item: { - _index: 1, - }, - }, + itemContext: { + id: "id1", + number: 1, + string: "hello", + boolean: true, + _index: 0, + _id: "id1", + _first: true, + _last: false, }, }; @@ -253,14 +96,14 @@ describe("TemplateVariablesService", () => { let service: TemplateVariablesService; let getNextCampaignRowsSpy: jasmine.Spy; - beforeEach(() => { + beforeEach(async () => { getNextCampaignRowsSpy = jasmine.createSpy(); TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [ { provide: TemplateFieldService, - useValue: new MockTemplateFieldService(), + useValue: new MockTemplateFieldService(MOCK_FIELDS), }, { provide: AppDataService, @@ -283,18 +126,56 @@ describe("TemplateVariablesService", () => { ], }); service = TestBed.inject(TemplateVariablesService); + await service.ready(); }); it("should be created", () => { expect(service).toBeTruthy(); }); - it("evaluates string containing item variable", async () => { - const value = await service.evaluatePLHData(MOCK_ITEM_STRING, MOCK_CONTEXT_WITH_ITEM_CTXT); - expect(value).toBe(1); + fit("Evaluates PLH Data String", async () => { + console.log({ TEST_FIELD_CONTEXT }); + const res = await service.evaluatePLHData("Hello @fields.string_field", TEST_FIELD_CONTEXT); + expect(res).toEqual("Hello test_string_value"); + // Data will only be evaluated if it has been pre-parsed, extracting dynamic references + // If not returns raw value + delete TEST_FIELD_CONTEXT.row._dynamicFields; + const resWithoutDynamicContext = await service.evaluatePLHData( + "@fields.string_field", + TEST_FIELD_CONTEXT + ); + expect(resWithoutDynamicContext).toEqual("@fields.string_field"); + /** + * TODO - include all edge cases, e.g. raw, item, calc, deep-nested, object, array etc. + const res = await service.evaluatePLHData(["@fields.string_field"], MOCK_CONTEXT); + expect(res).toEqual({ 1: "test_string_value" }); + const res = await service.evaluatePLHData( + { + nested: "@fields.string_field", + }, + MOCK_CONTEXT + ); + expect(res).toEqual({ nested: "test_string_value" }); + */ + }); + it("Evaluates condition strings", async () => { + // Condition strings are evaluated without any previous pre-parsed dynamic fields + const res = await service.evaluateConditionString("@fields.number_field > 3"); + expect(res).toEqual(false); }); - it("does not evaluate item string without appropriate context", async () => { - const value = await service.evaluatePLHData(MOCK_ITEM_STRING, MOCK_CONTEXT_WITHOUT_ITEM_CTXT); - expect(value).toBe(MOCK_ITEM_STRING); + + it("evaluates string containing item variable", async () => { + const MOCK_ITEM_STRING = "@item._index + 1"; + // Parse expression when item context included + const resWithItemContext = await service.evaluatePLHData(MOCK_ITEM_STRING, TEST_ITEM_CONTEXT); + expect(resWithItemContext).toEqual(1); + // Retain raw expression if evaluating outside of item context + // https://github.com/IDEMSInternational/parenting-app-ui/pull/2215#discussion_r1514757364 + delete TEST_ITEM_CONTEXT.itemContext; + const resWithoutItemContext = await service.evaluatePLHData( + MOCK_ITEM_STRING, + TEST_ITEM_CONTEXT + ); + expect(resWithoutItemContext).toEqual(MOCK_ITEM_STRING); }); }); From 04168d3f50e534932aa59bdfdff3e85809071d70 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 29 Apr 2024 22:46:03 -0700 Subject: [PATCH 30/30] chore: remove focused test --- .../template/services/template-variables.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/components/template/services/template-variables.service.spec.ts b/src/app/shared/components/template/services/template-variables.service.spec.ts index fbacc2c688..259296d750 100644 --- a/src/app/shared/components/template/services/template-variables.service.spec.ts +++ b/src/app/shared/components/template/services/template-variables.service.spec.ts @@ -133,7 +133,7 @@ describe("TemplateVariablesService", () => { expect(service).toBeTruthy(); }); - fit("Evaluates PLH Data String", async () => { + it("Evaluates PLH Data String", async () => { console.log({ TEST_FIELD_CONTEXT }); const res = await service.evaluatePLHData("Hello @fields.string_field", TEST_FIELD_CONTEXT); expect(res).toEqual("Hello test_string_value");