As described here, you can have a Stored Procedure returning data to a query. If you then would like to have row operations for this data, you will run into the issue that Aware does not have the BOs itself (see this post. As Jaymer pointed out in that post, you can make your own Row Operations via Javascript.
Below an example where I created a row operation "View" with a css class "ep-buttonview"
Code: Select all
widget.bind("dataBound", onDataBound);
function onDataBound(e) {
grid.tbody.find("a.aw-link.ep-buttonview").click(onViewClick);
}
function onViewClick(e) {
let currentRow = $(this).parents("tr");
let record = grid.dataItem(currentRow);
if (record) {
AwareApp.startProcess2(
"AwareProcessName",
"BOName",
record["BO_ID"],
"main"
);
}
}
- If there are multiple row operations, you need to create an event handler for each row
- The event handler must refer to the correct Aware Process. If the process name is changed, the render script must be updated accordingly. This makes the code harder to maintain and may create future bugs if you would change the configuration but forget to update the render script.
Generic event handler for row operation buttons displayed separately in each row
I developed the following alternative, which is fully generic. This means that the JS code does not need to have any knowledge about how many row operations are defined and what the required AIM process is for each operation.
Code: Select all
// Regular Expression to find the process name for data row operations. The first word after 'process:' in the itemOperand will be matched.
const processRegExp = new RegExp("(?<=process:)\\s*\\w+");
widget.bind("dataBound", onDataBound);
function onDataBound(e) {
grid.tbody.find("a.aw-link[data-operidx]").click(onDataOperationClick);
}
function onDataOperationClick(e) {
let currentRow = $(this).parents("tr");
let record = grid.dataItem(currentRow);
let itemOperIdx = Number($(this).attr("data-operidx"));
let processNameMatch =
parser.m_itemOpers[itemOperIdx].operand.match(processRegExp);
if (record && processNameMatch) {
try {
AwareApp.startProcess2(
processNameMatch[0].trim(),
"BOName",
record["BO_ID"],
"main"
);
} catch (e) {
console.log("Error calling process. Matched name: ", processNameMatch," - Error: ", e);
}
}
}
Because the regular Aware handler will also be triggered when clicking on the operation button, I put the information in a JS comment. This way, nothing happens when the Aware handler executes the Javascript. (In principle, it would be possible to attach an event handler to a lower DOM object and then prevent the event triggering at the higher level (no event bubbling), but that is more complicated and error-prone.)
Note that the custom javascript defined for the row operation can also contain other information or actual JS code (i.e. not commented out). This will be ignored by the event handler, but will of course be run by Aware if not commented out. For example, you could add additional clarification comments or even pass additional parameters to the event handler in the render script.
Each row operation button has an attribute "data-operidx", which is the index to the parser.m_itemOpers array. So by getting that index, the requested row operation can be found.
You need to ensure that each row in the grid contains the ID of the applicable BO that you need to pass to the AIM process. But the ID can be in a hidden column, so it isn't visible in the grid but can be easily accessed through the dataItem method for the grid.
Of course, the event handler must know what type of AIM object must be passed to the AIM process. But usually any query, although generated through the Stored Procedure, will be related to one existing AIM object so it is clear that this must be put into context for the row operation process.
Generic event handler for row operation buttons combined behind a single menu button
The above works fine for row operations that are specified as separate buttons on the row. It does not work if the row operations are included in one menu button, i.e. when the option "Create menu button for operations" is ticked (see screenshot above.). The reason is that this creates separate menus that are put in a different place in the DOM, not in the table rows. If row operations are combined behind a menu button, the code below can be used as a generic event handler:
Code: Select all
const processRegExp = new RegExp("(?<=process:)\\s*\\w+");
widget.bind("dataBound", onDataBound);
const menuButtons = {
menuIDs: [], // holds unique IDs for the row menus
operations: {}, // hashtable for the row operations (operation name, AIM process name)
listItems: "", // selector for the listitems in the menu, to make sure that only menubuttons that have an operation class are included
// Store operation names and corresponding process names in a hashtable
// Create a selector for the css classes of the menu buttons
// NOTE: each menu button must have a css class, but the class can be identical for all menu buttons
initialise: function () {
parser.m_itemOpers.forEach((operation) => {
let processNameMatch = operation.operand.match(processRegExp);
if (processNameMatch) {
menuButtons.operations[operation.operName] = processNameMatch[0].trim();
if (operation.cssClass && !(menuButtons.listItems.includes(operation.cssClass))) {
menuButtons.listItems += ", li." + operation.cssClass;
}
}
});
if (menuButtons.listItems.length > 2) {
menuButtons.listItems = menuButtons.listItems.substring(2); // Remove first ", "
}
// find all popup menus (one for each row) and store their IDs in an array
if (this.listItems) {
$("body")
.find('div.k-menu-popup[data-role="buttonmenu"] ul.k-menu-group')
.has(this.listItems)
.each(function () {
menuButtons.menuIDs.push($(this).attr("id"));
$(this).click(onDataOperationMenuClick);
});
}
},
};
function onDataBound(e) {
menuButtons.initialise();
}
function onDataOperationMenuClick(e) {
let rowIndex = menuButtons.menuIDs.indexOf($(this).attr("id"));
if (rowIndex >= 0) {
let currentRow = grid.tbody.find("tr").eq(rowIndex);
let record = grid.dataItem(currentRow);
let menuTextElement = $(e.target)
.closest("li")
.find("span.k-menu-link-text");
let operName = menuTextElement ? menuTextElement.text() : null;
if (record && operName && menuButtons.operations[operName]) {
try {
AwareApp.startProcess2(
menuButtons.operations[operName],
"BOName",
record["BO_ID"],
"main"
);
} catch (e) {
console.log("Error calling process. Matched name: ", processNameMatch," - Error: ", e);
}
}
}
}
The event handler also takes into account the possibility that for some rows, operations may be excluded as a result of the "Applicable" condition. If in the settings for the row operation an "Applicable" condition is set and for any particular row that condition is not fulfilled, the menu button for that operation will not be included in the generated HTML. This means that the index of the menu button cannot be used to find the relevant row operation. Instead, the event handler creates a hashtable of all row operations, with a key equal to the operation name and value equal to the process defined in the script settings. Note that this works only when the operation name is actually displayed on the button, otherwise it is not included in the HTML. But I think that you would always want to include the operation name on the button if the row operations are behind a separate row menu button. If only an image (icon) and no text would be displayed, it may make more sense to put those icons in the row instead of a separate menu.
Furthermore, note that you need to provide a custom css class for every row operation. This is used by the initialisation code in the script to ensure that the event handler is only bound to the custom menus for the row operations and not to any other menus that may be on the screen (for example top bar menu or left panel menu). But all row operations can have the same css class, they do not have to be different for each operation. If none of the row operation buttons for a particular record have a css class defined, that menu will not be included in the event handler and the menus for the other rows will not work correctly.
A few caveats
- Be sure to comment out the line 'process:[process name]' in the Javascript for the row operation
- Apply custom css class to each row operation
- Make sure that 'Display operation name on the button' is ticked
- Operation names must be unique for the same grid
Normally, if you change the name of a process then all references to that process are automatically updated by Aware. This does not apply to the reference in the javascript for the row operation (the line "// process: process name"), you will need to change that manually. And unfortunately that reference will not show up when you do a Search Version (Alt-V) for the name. So if you change the name of the process and forget where the reference(s) to that process are, you may have a hard time finding them. It's best to flag/document that the process should not be renamed without care, for example (as in the screen shot above) by adding JS to the process name to alert you that this is called from javascript. What I have done now is put all processes that are called directly from Javascript in a separate category (which becomes a folder under Processes) and added in the description of the process the the name of the query and the row operation that calls the process, to make it easier to find and amend the reference.
Summary
With this code you can create generic event handlers in the render script for queries displaying data output from a Stored Procedure. This enables row operations to be defined from the configurator. The regular AIM process receives the required BO as input, notwithstanding that the records in the query are not persisted in the database. You can even combine both code examples in the same render script, which has the effect that the script can deal with row operations regardless whether they are specified as separate buttons on the row or combined behind a single menu button.