Custom views refer to user-defined components designed to meet specific requirements that standard or build-in views cannot address. These are typically created to build entirely new ones with enhanced functionality or layouts. An overview when they should be used:
- to customize functionality as build-in views may lack required specific features or behaviors
- for reusability, once created, custom views can be reused consistently
- efficient code management, custom views encapsulate functionality and presentation for easier maintenance
The purpose of this article is to go through a process of creating an extension containing JavaScript code and styles that will be used as a renderer for custom view. Such view will show custom content representing board containing order details with color indicated statuses. Lastly we take a look on data formula that produces input for proposed extension.
Writing extension
Custom views are using extensions as its content renderer which we will focus on in this chapter. First we register one, see here for more information how its achieved. During creation choose custom view for its type. Now we will create 2 files custom-detail.js and custom-detail.css which will be uploaded into extension. We end up with following:
Proposed extension will render board with order details as seen on the beginning of this tutorial. Inside of our custom-detail.js we declare and implement main function or entry point of extension that we will come back to. Important to note here are its parameters, reference to jQuerry ($), custom view container reference that has this extension set to render content ($container) and configuration (conf) representing data input passed to extension. We will return to conf later as data formula while setting up a custom view. Other inputs are handled by the system and are there to be used inside extension. Second important thing is to set some content containing HTML structure for rendering into $container. For our example following implementation is proposed:
var customDetail = function ($, $container, conf) { $container = $container.closest('div:has(>.viewContentInnerLayout)'); $container.addClass('customDetail'); var $labels = $('<div/>').addClass('labels'); $container.append($labels); conf.labels.forEach(function (l) { $labels.append($('<div/>').addClass('label').text(l.text).prepend($('<span/>').css({ 'background-color': l.color }))); }) var $cols = $('<div/>').addClass('columns'); conf.columns.forEach(function (column) { var $column = $('<div/>').addClass('column'); const $ul = $("<ul/>").addClass("rows-list"); $column.append($('<h2/>').text(column.label)).append($ul); column.rows.forEach(function (row) { if (row != null) { var $ch = $('<li/>').addClass('row').css({ 'background-color': row.color }); $ch.text(row['L_ORDER_ID']); $ul.append($ch); }; }) $cols.append($column) }) $container.append($cols) }
Then we can add styling into custom-detail.css as follows:
.customDetail { overflow-y: auto; padding-inline: 10px; box-sizing: border-box; } .customDetail .labels { display: flex; justify-content: center; align-items: center; gap: 107px; margin: 7px 0 1em 0; text-transform: uppercase; font-size: 20pt; } .customDetail .labels div { display: flex; align-items: center; gap: 10px; } .customDetail .labels div span { display: inline-block; width: 49px; height: 18px; } .customDetail .columns { display: flex; gap: 24px 8px; flex-wrap: wrap; justify-content: center; overflow: hidden; } .customDetail .columns .column { flex: 0 0 auto; font-size: 1.2em; color: black; width: 168px; height: 100%; /* overflow-y: auto; */ } .customDetail .columns ul { overflow-y: auto; overflow-x: hidden; height: 95%; } .customDetail .columns .column .row { border-bottom: 1px solid #8c8c8c; padding: 2px 5px; font-size: 20px; } .customDetail .columns h2 { background: #444c5c; font-size: 24px; color: white; margin: 0; padding: 5px; text-transform: uppercase; }
Now to finish our extension declaration we need to register main function or entry point that will be ran on custom view loading. This is done editing parameters of extension as follows:
Lastly so that the extension is available and seen in system it needs to be activated in the Manage extensions page.
Writing formula
Prerequisite is to have an existing custom view in report. To create one follow this. During creation pick our extension as custom view renderer and at the same time its possible to set the data formula. If not we can edit view settings later. The data formula is a Groovy script where we can gain access to dataset data or any other we prepare ourselves and have the ability to manipulate it as needed. For more options about scripting and formulas in general see here. Return value is an object containing data that are passed into our chosen custom view renderer. The renderer for now is our extension and the data are passed via its main function or entry point as conf parameter. This was mentioned and configured in previous chapter.
In our case we will have separate dataset for order records and the structure will be as follows, attributes:
- order id : Text
- assembly date : Date
- assembly status : Text
- inspection date : Date
- inspection status : Text
- shipping date : Date
- shipping status : Text
For .*status attributes we will consider following values only: [Complete, In progress, Unable to start], when it comes to .*date attributes we will be querying those from the same day.
Now back to our custom view data formula, as mentioned using it we will prepare data for extension. For our example we will use following:
def currentDay = date(actualDate()); def columnConfigs = [ L_ASSEMBLY: 'ASSEMBLY', L_INSPECTION: 'INSPECTION', L_SHIPPING: 'SHIPPING' ]; def statusToColor = [ 'Complete': '#78A5A3', 'In progress': '#E1B16A', 'Unable to start': '#B7B7B7', ]; def orderedStatuses = ['Complete', 'In progress', 'Abnormal']; def columns = columnConfigs.collect { prefix, label -> def statusColumn = prefix + '_STATUS'; def todayFilter = isEqualFilter(prefix + '_DATE', currentDay); def statusFilter = isNotNullFilter(statusColumn); def orders = readDataset('ORDERS', -1, andFilter(todayFilter, statusFilter), null) .collect { def values = it.getValues(); def order = [color: statusToColor[values[statusColumn]]]; order.putAll(values); return order; } .sort { it['L_ORDER_ID'] } .groupBy { it[statusColumn] } def flattened = []; orderedStatuses.each { status -> flattened = flattened + (orders[status] ?: []) } return [label: label, rows: flattened] } return conf = [ labels: [ [ color: '#78A5A3', text: 'Completion'], [ color: '#E1B16A', text: 'Work in progress'], [ color: '#B7B7B7', text: 'Unable to start'] ], columns: columns, ]
It goes through each partial column reference name (columnConfigs), extends it as attributes in question have the same start of their name. Reads "Orders" dataset via readDataset() while applying today's time and non null status attribute filter (todayFilter, statusFilter). On each found record adds color entity (collect{}), then reading result is ordered by order id and grouped by status (sort{}, groupBy{}). Next collects such rows by status and label. Lastly passes predetermined label configuration and collected data into extension via return clause.
Extension can be found here:
Examples
A weather widget, uses external API calls inside extension to receive data and handles the design.