Skip to content
This repository was archived by the owner on Mar 19, 2024. It is now read-only.

feat: evaluate expression at runtime #455

Merged
merged 15 commits into from
Feb 5, 2024
Merged
5 changes: 5 additions & 0 deletions api-specifications/properties.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@
"type": "#/definitions/Action"
}
},
"runtimeExpressionEvaluation": {
"description": "Boolean to determine the parallel evaluation of expression. Set to true if you want the expression to be evaluated between the actions (instead of upfront).",
"kind": "literal",
"value": false
},
"navigation": {
"description": "Navigation action to move to new sheet/URL, performed after all other actions",
"type": "#/definitions/NavigationAction"
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/ext.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe("ext", () => {
]),
};
const props = ext({ translator, shouldHide, senseNavigation });
const actionItems = props.definition.items.actions.items.actions.items;
const actionItems = props.definition.items.actions.items.actionArray.items.actions.items;
const navigationItems = props.definition.items.actions.items.navigation.items;
const { icon } = props.definition.items.settings.items;
const {
Expand All @@ -38,7 +38,7 @@ describe("ext", () => {
});

describe("itemTitleRef", () => {
const { itemTitleRef } = props.definition.items.actions.items.actions;
const { itemTitleRef } = props.definition.items.actions.items.actionArray.items.actions;

beforeEach(() => {
data = {
Expand Down
58 changes: 53 additions & 5 deletions src/components/__tests__/action-button.spec.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import defaultValues from "../../__tests__/default-button-props";
import { renderButton, runActions } from "../action-button";

let actionList;
let actionCallList;
let button;
let defaults;
let app;

describe("action button", () => {
describe("renderButton", () => {
Expand All @@ -23,6 +24,8 @@ describe("action button", () => {
goToSheet: jest.fn(),
getCurrentStoryId: () => false,
};
defaults.model = { getProperties: jest.fn() };
defaults.app.evaluate = jest.fn();
});
it("should render action button", () => {
renderButton(defaults);
Expand Down Expand Up @@ -156,15 +159,60 @@ describe("action button", () => {
expect(button.removeAttribute).toHaveBeenCalledWith("disabled");
expect(defaults.senseNavigation.openOdagPopup).toHaveBeenCalledTimes(1);
});

it("should handle missing actions", async () => {
const actions = [
{ actionType: "missing", value: { qStringExpression: { qExpr: "missingExpression" } } },
{ actionType: "back", value: { qStringExpression: { qExpr: "missingExpression" } } },
];
defaults.layout = {
runtimeExpressionEvaluation: true,
actions,
};
defaults.app.back = jest.fn();
defaults.model = { getProperties: jest.fn().mockReturnValue({ actions }) };
renderButton(defaults);
await defaults.element.firstElementChild.onclick();
expect(defaults.app.back).toHaveBeenCalled();
expect(defaults.app.evaluate).toHaveBeenCalledTimes(2);
});
});
describe("runActions", () => {
const actions = [
{ value: { qStringExpression: { qExpr: "someExpression" } } },
{},
{ variable: { qStringExpression: { qExpr: "variableExpression" } } },
];
let model;
beforeEach(() => {
actionList = [jest.fn(), jest.fn()];
actionCallList = [jest.fn(), jest.fn(), jest.fn()];
app = { evaluate: jest.fn().mockReturnValue("Evaluated Expression") };
model = { getProperties: jest.fn().mockReturnValue({ actions }) };
});
it("should call all functions in array", async () => {
await runActions(actionList);
expect(actionList[0]).toHaveBeenCalledTimes(1);
expect(actionList[1]).toHaveBeenCalledTimes(1);
await runActions({ actionCallList, layout: {}, model, app });
expect(actionCallList[0]).toHaveBeenCalledTimes(1);
expect(actionCallList[1]).toHaveBeenCalledTimes(1);
expect(app.evaluate).not.toHaveBeenCalled();
});

it("should evaluate expressions when runtimeExpressionEvaluation is true", async () => {
await runActions({
actionCallList,
layout: {
runtimeExpressionEvaluation: true,
},
app,
model,
});
expect(actionCallList[0]).toHaveBeenCalledTimes(1);
expect(actionCallList[0]).toHaveBeenCalledWith("Evaluated Expression", undefined);
expect(actionCallList[1]).toHaveBeenCalledTimes(1);
expect(actionCallList[1]).toHaveBeenCalledWith(undefined, undefined);
expect(actionCallList[2]).toHaveBeenCalledTimes(1);
expect(actionCallList[2]).toHaveBeenCalledWith(undefined, "Evaluated Expression");
expect(app.evaluate).toHaveBeenCalledWith("someExpression");
expect(app.evaluate).toHaveBeenCalledWith("variableExpression");
});
});
});
45 changes: 29 additions & 16 deletions src/components/action-button.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
/* eslint-disable no-await-in-loop */
import allActions from "../utils/actions";
import navigationActions from "../utils/navigation-actions";
import styleFormatter from "../utils/style-formatter";

export const runActions = async (actionList) => {
for (let i = 0; i < actionList.length; i++) {
// eslint-disable-next-line no-await-in-loop
await actionList[i]();
export const runActions = async ({ actionCallList, model, layout, app }) => {
const properties = await model.getProperties();
for (let i = 0; i < actionCallList.length; i++) {
if (layout.runtimeExpressionEvaluation) {
const overrideValue =
properties.actions[i].value?.qStringExpression?.qExpr &&
Copy link
Collaborator

Choose a reason for hiding this comment

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

I have no idea what actionCallList does but, do note that overrideValue can become any falsy value. For example if qExpr === '', overrideValue becomes an empty string.

Same for overrideVariable.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Is actions a required property on properties?

Copy link
Collaborator

@cbt1 cbt1 Feb 2, 2024

Choose a reason for hiding this comment

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

And does it always match the length of actionCallList?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That is a couple of questions. In order:

  1. ActionCallList is the list of actions that need to run. Actions from actions get pushed into this array if they are found. Good catch. I'll look at that
  2. Yes it is.
  3. It does not have to match actually. Since we first try to find the actions from a list the actionCallList might be shorter than actions. This could happen if you create the button through the api. It also makes sure it is backwards compatible. Most important though is probably conversion from the old button. There are some actions that we do not support in the new if I remember correct

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed based on suggestion from Max. Also made sure that the actionCallList and actions list have the same length

(await app.evaluate(properties.actions[i].value.qStringExpression.qExpr));
const overrideVariable =
properties.actions[i].variable?.qStringExpression?.qExpr &&
(await app.evaluate(properties.actions[i].variable.qStringExpression.qExpr));
await actionCallList[i](overrideValue, overrideVariable);
} else {
await actionCallList[i]();
}
}
};

Expand All @@ -18,6 +29,7 @@ export const renderButton = ({
element,
multiUserAutomation,
translator,
model,
}) => {
const isSense = !!senseNavigation;
const button = element.firstElementChild;
Expand All @@ -42,20 +54,21 @@ export const renderButton = ({
const { actions } = layout;
actions.forEach((action) => {
const actionObj = allActions.find((act) => act.value === action.actionType);
actionObj &&
actionCallList.push(
actionObj.getActionCall({
app,
qStateName,
...action,
senseNavigation,
multiUserAutomation,
translator,
})
);
actionObj
? actionCallList.push(
actionObj.getActionCall({
app,
qStateName,
...action,
senseNavigation,
multiUserAutomation,
translator,
})
)
: actionCallList.push(() => {});
});
button.setAttribute("disabled", true);
await runActions(actionCallList);
await runActions({ actionCallList, model, layout, app });
if (senseNavigation && !senseNavigation.getCurrentStoryId()) {
const navigationObject = navigation && navigationActions.find((nav) => nav.value === navigation.action);
if (senseNavigation && navigationObject && typeof navigationObject.navigationCall === "function") {
Expand Down
Loading
Loading