Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: make local context available in data-items loop #2304

Closed
wants to merge 10 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,8 @@ export class TmplDataItemsComponent extends TemplateBaseComponent implements OnD
parsed[listKey] = listValue;
for (const [itemKey, itemValue] of Object.entries(listValue)) {
if (typeof itemValue === "string") {
parsed[listKey][itemKey] = await this.templateVariablesService.evaluateConditionString(
itemValue
);
parsed[listKey][itemKey] =
await this.templateVariablesService.evaluateConditionString(itemValue);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import { ICalcContext, TemplateCalcService } from "./template-calc.service";

const CALC_CONTEXT_BASE: ICalcContext = {
thisCtxt: {},
globalConstants: {},
globalFunctions: {},
};

export class MockTemplateCalcService implements Partial<TemplateCalcService> {
private calcContext: ICalcContext;
constructor(mockCalcContext?: Partial<ICalcContext>) {
this.calcContext = { ...CALC_CONTEXT_BASE, ...mockCalcContext };
}

public async ready(): Promise<boolean> {
return true;
}

public getCalcContext(): ICalcContext {
return {
thisCtxt: {},
globalConstants: {},
globalFunctions: {},
};
return this.calcContext;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ 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 { ICalcContext, TemplateCalcService } from "./template-calc.service";
import { MockTemplateCalcService } from "./template-calc.service.spec";
import clone from "clone";

const MOCK_APP_DATA = {};

Expand All @@ -32,10 +33,7 @@ const MOCK_CONTEXT_BASE: IVariableContext = {
calcContext: {
globalConstants: {},
globalFunctions: {},
thisCtxt: {
fields: MOCK_FIELDS,
local: {},
},
thisCtxt: {},
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Because calcContext gets overridden by templateCalcService.getCalcContext() as part of evaluatePLHString(), the values here has no effect on the results of evaluatePLHData() in current test cases

},
};

Expand All @@ -57,6 +55,17 @@ const TEST_FIELD_CONTEXT: IVariableContext = {
},
};

const MOCK_ITEM_CONTEXT: IVariableContext["itemContext"] = {
id: "id1",
number: 1,
string: "hello",
boolean: true,
_index: 0,
_id: "id1",
_first: true,
_last: false,
};

// Context adapted from this debug template:
// https://docs.google.com/spreadsheets/d/1tL6CPHEIW-GPMYjdhVKQToy_hZ1H5qNIBkkh9XnA5QM/edit#gid=114708400
const TEST_ITEM_CONTEXT: IVariableContext = {
Expand All @@ -76,18 +85,44 @@ const TEST_ITEM_CONTEXT: IVariableContext = {
],
},
},
itemContext: {
id: "id1",
number: 1,
string: "hello",
boolean: true,
_index: 0,
_id: "id1",
_first: true,
_last: false,
itemContext: MOCK_ITEM_CONTEXT,
};

const TEST_LOCAL_CONTEXT: IVariableContext = {
...MOCK_CONTEXT_BASE,
templateRowMap: {
string_local: {
name: "string_local",
value: "Jasper",
type: "set_variable",
_nested_name: "string_local",
},
},
row: {
...MOCK_CONTEXT_BASE.row,
value: "Hello @local.string_local",
_dynamicFields: {
value: [
{
fullExpression: "Hello @local.string_local",
matchedExpression: "@local.string_local",
type: "local",
fieldName: "string_local",
},
],
},
},
};

const TEST_LOCAL_CONTEXT_WITH_ITEM_CONTEXT = {
...TEST_LOCAL_CONTEXT,
itemContext: MOCK_ITEM_CONTEXT,
};

const MOCK_CALC_CONTEXT: Partial<ICalcContext> = {
thisCtxt: { local: { string_local: "Jasper2" } },
};

/**
* Call standalone tests via:
* yarn ng test --include src/app/shared/components/template/services/template-variables.service.spec.ts
Expand All @@ -111,7 +146,9 @@ describe("TemplateVariablesService", () => {
},
{
provide: TemplateCalcService,
useValue: new MockTemplateCalcService(),
// HACK: hardcoded calcContext from mock context is overridden by calcContext returned from MockTemplateCalcService,
// so insert values here for testing evaluation of local variable inside item loop
useValue: new MockTemplateCalcService(clone(MOCK_CALC_CONTEXT)),
},
// Mock single method from campaign service called
{
Expand Down Expand Up @@ -178,4 +215,23 @@ describe("TemplateVariablesService", () => {
);
expect(resWithoutItemContext).toEqual(MOCK_ITEM_STRING);
});

it("Evaluates string containing local variable", async () => {
const MOCK_LOCAL_STRING = "Hello @local.string_local";
const resWithLocalContext = await service.evaluatePLHData(
MOCK_LOCAL_STRING,
TEST_LOCAL_CONTEXT
);
expect(resWithLocalContext).toEqual("Hello Jasper");
});

it("Evaluates string containing local variable, inside item loop", async () => {
const MOCK_LOCAL_STRING = "Hello @local.string_local";
// When itemContext is included (i.e. in an item loop), look to thisCtxt for the parsed local var
const resWithLocalContext = await service.evaluatePLHData(
MOCK_LOCAL_STRING,
TEST_LOCAL_CONTEXT_WITH_ITEM_CONTEXT
);
expect(resWithLocalContext).toEqual("Hello Jasper2");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -332,36 +332,48 @@ export class TemplateVariablesService extends AsyncServiceBase {
// TODO - assumed 'value' field will be returned but this could be provided instead as an arg
const returnField: keyof FlowTypes.TemplateRow = "value";

// find any rows where nested path corresponds to match path
let matchedRows: { row: FlowTypes.TemplateRow; nestedName: string }[] = [];
Object.entries(templateRowMap).forEach(([nestedName, row]) => {
if (nestedName === fieldName || nestedName.endsWith(`.${fieldName}`)) {
matchedRows.push({ row, nestedName });
}
});
// no match found. If condition assume this is fine, otherwise authoring error
if (matchedRows.length === 0) {
if (field === "condition") {
parsedValue = false;
} else {
// In a data-items loop, the templateRowMap is only of the items rows.
// In this case, we can look at the calcContext to see if the local variable value has already been parsed, and return this value
if (context.itemContext && field !== "condition") {
parsedValue = context.calcContext.thisCtxt?.local?.[fieldName];
if (!parsedValue && parsedValue !== 0) {
parseSuccess = false;
console.error(`@local.${fieldName} not found`, {
evaluator,
rowMap: templateRowMap,
});
}
}
// match found - return least nested (in case of duplicates)
else {
matchedRows = matchedRows.sort(
(a, b) => a.nestedName.split(".").length - b.nestedName.split(".").length
);
if (matchedRows.length > 1) {
console.warn(`@local.${fieldName} found multiple`, { matchedRows });
} else {
// find any rows where nested path corresponds to match path
let matchedRows: { row: FlowTypes.TemplateRow; nestedName: string }[] = [];
Object.entries(templateRowMap).forEach(([nestedName, row]) => {
if (nestedName === fieldName || nestedName.endsWith(`.${fieldName}`)) {
matchedRows.push({ row, nestedName });
}
});
// no match found. If condition assume this is fine, otherwise authoring error
if (matchedRows.length === 0) {
if (field === "condition") {
parsedValue = false;
} else {
parseSuccess = false;
console.error(`@local.${fieldName} not found`, {
evaluator,
rowMap: templateRowMap,
});
}
}
// match found - return least nested (in case of duplicates)
else {
matchedRows = matchedRows.sort(
(a, b) => a.nestedName.split(".").length - b.nestedName.split(".").length
);
if (matchedRows.length > 1) {
console.warn(`@local.${fieldName} found multiple`, { matchedRows });
}
parsedValue = matchedRows[0].row[returnField];
}
parsedValue = matchedRows[0].row[returnField];
}

break;
case "field":
// console.warn("To keep consistency with rapidpro, @fields should be used instead of @field");
Expand Down
Loading