Projects:Alert Management Redesign/Complex view using static JavaScript example
Contents |
Objective
The objective of this article is to provide a complex example of a view using static JavaScript based on the How to add a View Implementation. This article takes as example the redesign of the Alert Management view.
![]() | Note: This article assumes that you have a basic knowledge of SmartClient |
![]() | Note: Code examples shown on the article are extracts of the complete JavaScript file, they might not work correctly on their own |
Implementation
Create an empty view (OBUIAPP_AlertManagement)
In the main how to is described how to add a Label. On this example it is used a new class extending VLayout. The isSameTab and getBookMarkParams functions are implemented assuming that it is not possible to open more than one tab with the view.
isc.ClassFactory.defineClass('OBUIAPP_AlertManagement', isc.VLayout); OBUIAPP_AlertManagement.addProperties({ isSameTab: function(viewId, params){ return viewId === 'OBUIAPP_AlertManagement'; }, getBookMarkParams: function() { var result = {}; result.viewId = 'OBUIAPP_AlertManagement'; return result; } }
Once the new JavaScript file is registered on the corresponding ComponentProvider as a StaticResource we can open the empty view on a new tab. As this view belongs to the Client Application module the file is included on its ComponentProvider.
Add content
With an empty view it's time to start adding the content. The content will differ a lot depending the view design and the requirements. On this how-to is demonstrated the example of the Alert Management that has it's own requirements. Other developments will need different SmartClient components or existing Openbravo's components.
The structure of the view is a VLayout which has 2 members, the top one including the Toolbar and the main including a SectionStack with the alerts. The SectionStack contains a section for each Alert Status showing a grid with the alerts in the corresponding statuses. Following is a description on how is built the view on an incremental manner.
The SectionStack
Instead of using directly the SmartClient's class it is used the OBSectionStack class that extends it. The benefit of this approach is that we can set up common properties and styles and reuse them later.
The Section Stack is created on the initWidget function of the main view. As this view can only be opened once per client a global variable OB.AlertManagement is created to be used later to access the view instance at any point. The translated labels of the alert statuses are stored in core, so they are retrieved using the OB.I18N.getLabel function using as parameters the message value, the object (translatedStatus) and property where the translation has to be stored. The sectionStack and the sections are stored as objects of the view to be easily accessed afterwards. Once the Section Stack is created the createSections() function is called that at this point just creates 4 empty sections, one for each alert status. The unique section that is expanded by default is the first one.
OBUIAPP_AlertManagement.addProperties({ translatedStatus: { New: '', Acknowledged: '', Suppressed: '', Solved: ''}, sectionStack: null, sections: {}, initWidget: function(){ OB.AlertManagement = this; OB.I18N.getLabel('AlertStatus_New', null, this.translatedStatus, 'New'); OB.I18N.getLabel('AlertStatus_Acknowledged', null, this.translatedStatus, 'Acknowledged'); OB.I18N.getLabel('AlertStatus_Suppressed', null, this.translatedStatus, 'Suppressed'); OB.I18N.getLabel('AlertStatus_Solved', null, this.translatedStatus, 'Solved'); this.sectionStack = isc.OBSectionStack.create(); this.addMember(this.sectionStack); this.createSections(); this.Super('initWidget', arguments); }, createSections: function() { this.sections.New = { title: this.translatedStatus.New, alertStatus: 'New', expanded: true, items: [], controls: []}; this.sectionStack.addSection(this.sections.New); this.sections.Acknowledged = { title: this.translatedStatus.Acknowledged, alertStatus: 'Acknowledged', expanded: false, items: [], controls: []}; this.sectionStack.addSection(this.sections.Acknowledged); this.sections.Suppressed = { title: this.translatedStatus.Suppressed, alertStatus: 'Suppressed', expanded: false, items: [], controls: []}; this.sectionStack.addSection(this.sections.Suppressed); this.sections.Solved = { title: this.translatedStatus.Solved, alertStatus: 'Solved', expanded: false, items: [] }; this.sectionStack.addSection(this.sections.Solved); } });
The action handler
Alert Rules might have SQL filters defined, this filters can not be applied to grid's DataSources, so we have to apply those filters some how. The alert rules have also Alert Recipients which define how will get the alerts generated by the alert rule. So grids must also be filtered to show only user accessible alert rules. To apply these two filters is it used an Action Handler.
When the view is loaded a call to the server is done to get the alert rules that the context's user and role can view. And in case that the alert rule has a filter expression the list of alert ids to show on the grids. This is stored as an object of the view alertRules. As we need the alert rules to filter the grids, the sections are created when these are returned on the callback function.
initWidget: function(){ ... this.Super('initWidget', arguments); this.getAlertRules(); }, getAlertRules: function() { post = {'eventType': 'getAlertRules' }; OB.RemoteCallManager.call('org.openbravo.client.application.AlertManagementActionHandler', post, {}, function(rpcResponse, data, rpcRequest){ OB.AlertManagement.alertRules = data.alertRules; // Sections are created after alertRules are created. This is needed to be able to filter // properly the grids of the sections. OB.AlertManagement.createSections(); }); },
The execute method of the Action Handler checks the eventType and calls the getAlertRules() method. This method gets the Alert Rule list filtering by the alert recipients. For each alert rule accessible by the user is returned a JSON Object with the name, tabId and alertRuleId. And if the alert rule has a filter expression it is executed using SQLQuery to get the active alerts which are included in the JSON Object as a String to be used in an IN statement.
protected JSONObject execute(Map<String, Object> parameters, String content) { JSONObject object = new JSONObject(); OBContext.setAdminMode(); try { JSONObject o = new JSONObject(content); final String strEventType = o.getString("eventType"); if (GET_ALERT_RULES.equals(strEventType)) { object.put("alertRules", getAlertRules()); } else { log.error("Unsupported event type: " + strEventType); } } catch (JSONException e) { log.error("Error executing action: " + e.getMessage(), e); } finally { OBContext.restorePreviousMode(); } return object; }
The grid
The grid is defined as a new class OBAlertGrid on a separate JavaScript file extending OBGrid. The 4 grids will be equal just showing different alert statuses. So 4 instances of the same OBAlertGrid class are used on the view.
Some extra properties are set to configure the grid to the requirements of the Alert Management window:
isc.ClassFactory.defineClass('OBAlertGrid', isc.OBGrid); isc.OBAlertGrid.addProperties({ alertStatus: null, width: '100%', height: '100%', dataSource: null, canEdit: true, alternateRecordStyles: true, showFilterEditor: true, canReorderFields: false, canFreezeFields: false, canGroupBy: false, canAutoFitFields: false, selectionType: 'simple', editEvent: 'click', //editOnFocus: true, showCellContextMenus: true, dataProperties: { useClientFiltering: false//, //useClientSorting: false },
The grid have static fields so they are defined on the class. Only the Note field is editable and all of them are filterable. The type matches the reference defined on the columns on Application Dictionary. '_id_19', matches tableDir which is ad_reference_id = '19'. '16' is DateTime and '10' is Text. As the Note field is the only one editable it has defined the editorType and editorTypeProperties.
gridFields: [ { name: 'alertRule', title: OB.I18N.getLabel('OBUIAPP_AlertGrid_AlertRule'), displayField: 'alertRule._identifier', canFilter: true, canEdit: false, filterOnKeypress: true, filterEditorType: 'OBFKFilterTextItem', type: '_id_19' }, { name: 'description', title: OB.I18N.getLabel('OBUIAPP_AlertGrid_Alert'), canFilter: true, canEdit: false, filterOnKeypress: true, filterEditorType: 'OBTextItem' //, type: '_id_10' }, { name: 'creationDate', title: OB.I18N.getLabel('OBUIAPP_AlertGrid_Time'), canFilter: true, canEdit: false, filterEditorType: 'OBMiniDateRangeItem', type: '_id_16' }, { name: 'comments', title: OB.I18N.getLabel('OBUIAPP_AlertGrid_Note'), canFilter: true, canEdit: true, filterOnKeypress: true, filterEditorType: 'OBTextItem', editorType: 'OBTextItem', editorProperties: { width: '90%', columnName: 'comments', disabled: false, updatable: true } //, type: '_id_10' }, { name: 'recordID', title: OB.I18N.getLabel('OBUIAPP_AlertGrid_Record'), canFilter: true, canEdit: false, isLink: true, filterOnKeypress: true, filterEditorType: 'OBTextItem' //, type: '_id_10' } ],
Some other properties are set when the grid is initialized:
- Defining the this.checkboxFieldDefaults is enabled the column with checkboxes to handle selections.
- The context Menu is initialized without any option. These are added later when actions are defined.
- And finally the datasource is loaded using the OB.Datasource.get('ADAlert', this) function. This one calls the setDataSource function on its callback. This function is implemented as well to reload the fields. The initial sort on the creationDate field is also set here. Finally is done a fetch to load the data on the grid.
initWidget: function() { // added for showing counts in the filtereditor row this.checkboxFieldDefaults = isc.addProperties(this.checkboxFieldDefaults, { canFilter: true, frozen: true, canFreeze: true, showHover: true, prompt: OB.I18N.getLabel('OBUIAPP_GridSelectAllColumnPrompt'), filterEditorProperties: { textAlign: 'center' }, filterEditorType: 'StaticTextItem' }); this.contextMenu = this.getMenuConstructor().create({items: []}); OB.Datasource.get('ADAlert', this); this.Super('initWidget', arguments); }, setDataSource: function() { this.Super('setDataSource', arguments); // Some properties need to be set when the datasource is loaded to avoid errors when form is // open the first time. this.setFields(this.gridFields); this.setSelectionAppearance('checkbox'); this.sort('creationDate', 'descending'); this.fetchData(); },
As said before, some filtering needs to be done on the grid depending on the Alert Status. To do so it is used the onFetchData function allows to modify the requestProperties. This function is called every time a fetch request is done to the server. As it is desired to filter the grid the OB.Constants.WHERE_PARAMETER is set. This paremeter is loaded by Openbravo's datasources and has to be a valid HQL where clause. The getFilterClause function uses the OB.AlertManagement.alertRules object set by the Action Handler. It iterates through it to build an IN statement for the alert rules ids that the user has access to. And in case that an alert rule has a filter expression defined another IN clause for the alert ids of that alert rule.
onFetchData: function(criteria, requestProperties){ requestProperties = requestProperties || {}; requestProperties.params = requestProperties.params || {}; requestProperties.params[OB.Constants.WHERE_PARAMETER] = this.getFilterClause(); }, getFilterClause: function() { var i, filterClause = '', alertRuleIds = '', arlength = OB.AlertManagement.alertRules.length, whereClause = 'status = upper(\'' + this.alertStatus + '\')'; for (i = 0; i < arlength; i++) { if (alertRuleIds !== '') { alertRuleIds += ','; } alertRuleIds += '\'' + OB.AlertManagement.alertRules[i].alertRuleId +'\''; // if an alertRule has some alerts to filter by, add them to the where clause as: // alerts are of a different alertRule or only the alerts predefined // this only happens if the alertRule has an SQL filter expression defined if (OB.AlertManagement.alertRules[i].alerts) { filterClause += ' and (e.alertRule.id != \'' + OB.AlertManagement.alertRules[i].alertRuleId + '\''; filterClause += ' or e.id in (' +OB.AlertManagement.alertRules[i].alerts + '))'; } } whereClause += ' and alertRule.id in (' + alertRuleIds + ')'; if (filterClause !== '') { whereClause += filterClause; } return whereClause; },
Some other functions needs to be implemented to have a working editable grid:
- clearFilter: function to clear the filters of the grid fields. Notice that the where expression will always be applied as it is set as a Request Parameter on each fetch.
- selectionChanged and updateSelectedCountDisplay: functions to set and update the number of selected records on the checkboxes field.
- setFieldProperties and cellHoverHTML: functions to set a hover on the checkbox field.
clearFilter: function(){ delete this.filterClause; this.filterEditor.getEditForm().clearValues(); this.filterEditor.performAction(); }, selectionChanged: function(record, state){ this.updateSelectedCountDisplay(); this.Super('selectionChanged', arguments); }, updateSelectedCountDisplay: function(){ var selection = this.getSelection(); var selectionLength = selection.getLength(); var newValue = ' '; if (selectionLength > 0) { newValue = selectionLength + ''; } if (this.filterEditor) { this.filterEditor.getEditForm().setValue(this.getCheckboxField().name, newValue); } }, // overridden to support hover on the header for the checkbox field setFieldProperties: function(field, properties){ var localField = field; if (isc.isA.Number(localField)) { localField = this.fields[localField]; } if (this.isCheckboxField(localField) && properties) { properties.showHover = true; properties.prompt = OB.I18N.getLabel('OBUIAPP_GridSelectAllColumnPrompt'); } return this.Super('setFieldProperties', arguments); }, cellHoverHTML: function(record, rowNum, colNum){ var field = this.getField(colNum), cellErrors, msg = '', i; if (this.isCheckboxField(field)) { return OB.I18N.getLabel('OBUIAPP_GridSelectColumnPrompt'); } } });
Updating the section titles
The last step is to add a counter on each section header with the number of alerts on the corresponding status. For that purpose is created the setTotalRows. This is a very simple function that receives the number of alerts and their status and updates the title of the corresponding title. An if is needed as in some scenarios this function is called before the section has been created. The title is translated using the OB.I18N.getLabel function. It retrieves a very simple message OBUIAPP_AlertSectionHeader defined like %0, (%1), where %0 is the status and %1 the number.
setTotalRows: function(totalRows, status) { if (OB.AlertManagement.sections[status]) { OB.AlertManagement.sections[status].getSectionHeader() .setTitle(OB.I18N.getLabel('OBUIAPP_AlertSectionHeader', [OB.AlertManagement.translatedStatus[status], totalRows])); } },
The setTotalRows function is called from grid's getGridTotalRows function. This checks if it's section is exapanded to determine if it is possible to use grid's this.getTotalRows() function or is needed a call to the dataSource's this.dataSource.fetchData function to get the number of rows. The getGridTotalRows is called from the dataArrived function which is called each time the server returns data to print on the grid.
dataArrived: function(startRow, endRow){ this.getGridTotalRows(); return this.Super('dataArrived', arguments); }, getGridTotalRows: function(){ var criteria = this.getCriteria() || {}, requestProperties = {}; if (!OB.AlertManagement.sections[this.alertStatus].expanded) { // fetch to the datasource with an empty criteria to get all the rows requestProperties.params = requestProperties.params || {}; requestProperties.params[OB.Constants.WHERE_PARAMETER] = this.getFilterClause(); requestProperties.clientContext = {alertStatus: this.alertStatus}; this.dataSource.fetchData(criteria, function(dsResponse, data, dsRequest){ OB.AlertManagement.setTotalRows(dsResponse.totalRows, dsResponse.clientContext.alertStatus); }, requestProperties ); } else { OB.AlertManagement.setTotalRows(this.getTotalRows(), this.alertStatus); } },
The toolbar
To add the toolbar we use the existing OBToolbar class. A new instance of it is created on the initWidget function of the view and it is added to the layout before the SectionStack to appear on the top of the page. To do so we have to provide the view instance that contains the toolbar, and 2 arrays with the buttons we want on the left and right. On this example only the Refresh button is available. This button requires to implement the refresh function on the view that contains the toolbar. On this case the grids are forced to reload using the invalidateCache() function. And in case that the section is not expanded the getGridTotalRows() function is called to update the section title with the current number of alerts.
initWidget: function(){ OB.AlertManagement = this; this.addMember(isc.OBToolbar.create({ view: this, leftMembers: [isc.OBToolbarIconButton.create(isc.OBToolbar.REFRESH_BUTTON_PROPERTIES)], rightMembers: [] })); this.sectionStack = isc.OBSectionStack.create(); this.addMember(this.sectionStack); this.Super('initWidget', arguments); this.getAlertRules(); }, ... ... // Removed code ... refresh: function() { var i, alertStatus = ['New', 'Acknowledged', 'Suppressed', 'Solved']; for (i = 0; i < 4; i++) { OB.AlertManagement.grids.alertStatus[i].invalidateCache(); if (!OB.AlertManagement.sections.alertStatus[i].expanded) { OB.AlertManagement.grids.alertStatus[i].getGridTotalRows(); } } }
Adding actions
The next step is to develop the ability to change the alert status. The user has to be able to move the alerts from New, Acknowledged and Suppressed statuses as desired. There are 2 ways to update alert statuses on this view. One by one using the mouse right button on the grid's alert record using a context menu. Or after selecting one or more alerts through links on the section headers.
The status update in both cases is done through the Action Handler using a new event type, moveToStatus. On each call to the handler it's included the alert ids to be updated, the previous status and the new status. Following the code already described a new else if is added on the execute() method. This retrieves all the information needed and calls the setNewStatus method. Once the status in updated the old and new statuses are put on the return JSONObject. The setNewStatus converts the comma separated String of alert ids to a List object with all the alerts and iterates through it to set the new status. It finally does a flush to persist the changes on the database.
if (GET_ALERT_RULES.equals(strEventType)) { object.put("alertRules", getAlertRules()); } else if (MOVE_TO_STATUS.equals(strEventType)) { final String alertIDs = o.getString("alertIDs"); final String oldStatus = o.getString("oldStatus"); final String newStatus = o.getString("newStatus"); setNewStatus(alertIDs, newStatus); object.put("oldStatus", oldStatus); object.put("newStatus", newStatus); } else { log.error("Unsupported event type: " + strEventType); } ... ... private void setNewStatus(String alertIDs, String newStatus) { if (StringUtils.isEmpty(alertIDs)) { return; } List<Alert> alerts = OBDao.getOBObjectListFromString(Alert.class, alertIDs); for (Alert alert : alerts) { alert.setAlertStatus(newStatus.toUpperCase()); OBDal.getInstance().save(alert); } OBDal.getInstance().flush(); }
On the javascript file is added the moveToStatus function. This is called from the grid context menu and from the links on the section headers with the information needed to set on the Action Handler. It does the call and on the callback function reloads the old and new statuses grids using its invalidateCache function. If the new status hasn't been expanded yet the grid is still not loaded, so a call to the getGridTotalRows is needed to update the count on the section header.
moveToStatus: function(alertIDs, oldStatus, newStatus) { post = {'eventType': 'moveToStatus', 'oldStatus': oldStatus, 'newStatus': newStatus, 'alertIDs': alertIDs}; OB.RemoteCallManager.call('org.openbravo.client.application.AlertManagementActionHandler', post, {}, function(rpcResponse, data, rpcRequest){ OB.AlertManagement.grids[data.newStatus].invalidateCache(); // If section has not been expanded the grid is not reloaded so the total rows is not updated. if (!OB.AlertManagement.sections[data.newStatus].expanded) { OB.AlertManagement.grids[data.newStatus].getGridTotalRows(); } // Old status is always expanded to be able to select the rows OB.AlertManagement.grids[data.oldStatus].invalidateCache(); }); },
To populate the grid's context menus is used the makeCellContextItems function. There are 3 possible to move alerts to New, Acknowledged or Supressed status. They are added to the grid based on the status that it is showing. So, if the grid is showing alerts in New status it will add the menuItem to move alerts to Acknowledged and Suppressed status.
Each menuItem contains a title and a click definition. The title is populated using the OB.I18N.getLabel function. In this case it's loading the OBUIAPP_MoveToStatus message that has a parameter for the status. In the ad_message the message is stored as Move alert to %0 where the %0 is replaced by the first parameter on the parameters array. The click definition is a function to be executed when the menuItem is clicked. On this case it calls the moveToStatus function previously described.
makeCellContextItems: function(record, rowNum, colNum){ var menuItems = []; var grid = this; if (grid.alertStatus === 'Acknowledged' || grid.alertStatus === 'Suppressed') { menuItems.add({ title: OB.I18N.getLabel('OBUIAPP_MoveToStatus', ['New']), click: function(){ OB.AlertManagement.moveToStatus(record.id, grid.alertStatus, 'New'); } }); } if (grid.alertStatus === 'New' || grid.alertStatus === 'Suppressed') { menuItems.add({ title: OB.I18N.getLabel('OBUIAPP_MoveToStatus', ['Acknowledged']), click: function(){ OB.AlertManagement.moveToStatus(record.id, grid.alertStatus, 'Acknowledged'); } }); } if (grid.alertStatus === 'New' || grid.alertStatus === 'Acknowledged') { menuItems.add({ title: OB.I18N.getLabel('OBUIAPP_MoveToStatus', ['Suppressed']), click: function(){ OB.AlertManagement.moveToStatus(record.id, grid.alertStatus, 'Suppressed'); } }); } return menuItems; }
The links on the Section headers are set as controls on each section definition of the SectionStack. On this case is created the OBAlertSectionStackControl with extends existing OBSectionItemControlLink to add the action to perform when the link is clicked. The OBSectionItemControlLink is a class with the desired styles extending SmartClient's Label class. The initWidget of the class is used to set the text of the link, using again the OB.I18N.getLabel function with a parameter.
The action function retrieves the selected records of the corresponding grid. With the selected records is created a comma separated string with the alert ids and is called the moveToStatus function.
isc.ClassFactory.defineClass('OBAlertSectionStackControl', isc.OBSectionItemControlLink); isc.OBAlertSectionStackControl.addProperties({ newStatus: null, currentStatus: null, initWidget: function () { this.setContents(OB.I18N.getLabel('OBUIAPP_MoveSelectedToStatus', [this.newStatus])); this.Super('initWidget', arguments); }, action: function() { var i, alerts = '', selectedAlerts = OB.AlertManagement.grids[this.currentStatus].getSelection(), selAlertsLength = selectedAlerts.length; if (selAlertsLength === 0) { return; } for (i = 0; i < selAlertsLength; i++) { if (alerts !== '') { alerts += ','; } alerts += selectedAlerts[i].id; } OB.AlertManagement.moveToStatus(alerts, this.currentStatus, this.newStatus); }
The controls are added on the previously described createSections function. Below is the example of the New section. The controls are created and included in an array on the section.
createSections: function() { this.grids.New = isc.OBAlertGrid.create({alertStatus: 'New'}); this.NewAcknowledged = isc.OBAlertSectionStackControl.create({currentStatus: 'New', newStatus: 'Acknowledged', ID:'NewAcknowledged'}); this.NewSuppressed = isc.OBAlertSectionStackControl.create({currentStatus: 'New', newStatus: 'Suppressed', ID:'NewSuppressed'}); this.sections.New = { title: OB.I18N.getLabel('AlertStatus_New'), alertStatus: 'New', expanded: true, items: [this.grids.New], controls: [this.NewAcknowledged, this.NewSuppressed]}; this.sectionStack.addSection(this.sections.New); ... }