- Home [object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]
- Getting started
- Manage types
- Global context and isolation
- Manage apps
- Batch actions with app items
- Manage external services
- Scripts in widgets
- Web components
- Access permissions
- Getting started with processes
- Getting started with signatures
- Getting started with file previews
- Getting started with the organizational chart
- Getting started with users and groups
-
Getting started with the
Table data type - Dynamic calculation of event type
- Use cases
- How to set up custom view of app items
- How to set up dynamic display of fields and widgets with a complex condition
- How to register a document
- How to calculate the number of days between two dates
- How to create a substitution for a user
- How to use pagination and sorting when searching for app items
- API
- Object types
- Data types
- Global constants
- Work with apps
- Web requests
- Access permissions
- Document flow
- Live Chats
- “Code” widget
- Signatures
- Business calendars
- Integration with IP telephony
- Integration with email marketing services
In this article
How to set up custom view of app items
Sometimes the list of items in an app needs to be presented in a more convenient way than it can be displayed by default. Moreover, data from other apps associated with these items may need to be available as well. This can be needed to set up dynamic reports, dashboards, etc.
Let’s say there is an app named Materials in the company. It stores a list of materials for construction and installation works.
The company also has the Orders app that stores orders from clients for the delivery of materials. The app’s form includes a Table type property that is filled out with ordered goods.
We need to create a separate summary table that would show which materials are needed for different orders. This table needs to illustrate what amount of each material is needed to fulfill all orders and to fulfill each one separately.
To do that, let’s create a new page and name it Ordered materials. In the beginning, we only need the Code widget to be added to the page. It will be used to set the view of items that we need.
Script
Planning the organization of data
To make the coding of the page’s layout easier, we first need to determine what we need the result to look like. Based on the data organization we need, we’ll create interfaces that we can gradually enhance. For example, we know that we’ll need the names of materials (
materialName
) and the amount of each material needed to fulfill orders (quantity
). We’ll also need to see the list of orders that include each material (deals
).interface NeedForMaterials { materialName: string; quantity: number; deals: any[]; }
We don’t need to display detailed information about orders on this page. We only need to know the name of each order (
dealName
) and the amount of each material (quantity
). Let’s also add the order’s ID to accurately identify each order if needed:interface NeedByDeal { dealId: string; dealName: string quantity: number; }
Now we’ll modify the main interface, specifying which type the array of data on orders will have:
interface NeedForMaterials { materialName: string; quantity: number; deals: NeedByDeal[]; }
In the interface designer, we need to open the Scripts tab. In our case, the script only needs to run on the client side. Let’s add the interfaces we created to the code.
To store and process the list of ordered materials in a convenient manner, let’s create the
materialNeeds
property. It will be an array of ordered materials, which will allow us to use methods available for arrays. For instance, they can be used for filtering:let materialNeeds: NeedForMaterials[];
Getting and processing data
Let’s write a function that fills the list of ordered goods with data:
Using the Namespace global constant, let’s call the
search()
method for the apps we need, and limit the number of items to 1,000 (the maximum possible number is 10,000):const materials = await Namespace.app.materials.search().size(1000).all(); const deals = await Namespace.app.deals.search().size(1000).all();
Now we have two independent asynchronous requests, and they need to be made at the same time. Here it is most efficient to run them simultaneously. Let’s use the
Promise.all()
method that unites an array of promises into one promise, wait for the result, and decompose it.const [materials, deals] = await Promise.all([ Namespace.app.materials.search().size(1000).all(), Namespace.app.deals.search().size(1000).all(), ]);
materialsList
) is empty in a certain order, this step will be skipped for this app item, and the next one will be checked. If the value of this property is defined, we need to iterate over the table’s rows. They don’t have all the data, but we can extract a material’s ID from them and use it to find a material in the list.for (const deal of deals) { if (!deal.data.materialsList) { continue; } for (const tableItem of deal.data.materialsList) { const material = materials.find(material => material.id === tableItem.materialsList.id); } }
NeedByDeal
:const dealNeeds = <NeedByDeal> { dealId: deal.id, dealName: deal.data.__name, quantity: tableItem.materialAmount, };
materialNeeds
). In the row of the tabletableItem
, there is a column of the App type that is linked with Materials (materialsList
). We can get the ID we need from it. Let’s use it to check whether a specific item has already been added to the list of ordered items. If it doesn’t exist in the list, let’s add it, specifying general information.Then we need to organize information about materials needed to fulfill a specific order
dealNeeds
. Let’s add this information to the list of orders on a specific material. We need to increase the amount of a material that is needed for all orders by the amount needed for each specific order.const material = materials.find(material => material.id === tableItem.materialsList.id); let need: NeedForMaterials | undefined = materialNeeds.find(need => need.materialId === tableItem.materialsList.id); if (!need) { need = { materialId: tableItem.materialsList.id, materialName: material!.data.__name, quantity: 0, deals: [], showDeals: true, }; materialNeeds.push(need); } const dealNeeds = <NeedByDeal> { dealId: deal.id, dealName: deal.data.__name, quantity: tableItem.materialAmount, clientId: deal.data.client?.id, }; need.quantity += tableItem.materialAmount; need.deals.push(dealNeeds);
Generating the result
As we get items asynchronously in the function that fills the table, the function itself is asynchronous. But as the page is loading, we need to wait for the function to finish. Otherwise, nothing will be displayed in the table when the page loads, and then, we’ll need to update the table. In our case, it is not needed, so we’ll wait for the result while the page is initializing (loading).
To do that, we need to create another function in the widget’s scripts. This function needs to be executed upon the page’s initialization. There we’ll add the asynchronous function that fills out the table and wait for its result. Then all data will be prepared by the time the page is actually rendered.
To get data from scripts in a more convenient way, let’s add a function and name it
getNeeds()
. It will return the current value ofmaterialNeeds
.The final version of the client script looks like this:
let materialNeeds: NeedForMaterials[]; interface NeedForMaterials { materialName: string; quantity: number; deals: NeedByDeal[]; } interface NeedByDeal { dealId: string; dealName: string quantity: number; } async function onInit(): Promise<void> { await fillNeeds(); } function getNeeds() { return materialNeeds; } async function fillNeeds() { materialNeeds = []; const [materials, deals] = await Promise.all([ Namespace.app.materials.search().size(1000).all(), Namespace.app.deals.search().size(1000).all(), ]); for (const deal of deals) { if (!deal.data.materialsList) { continue; } for (const tableItem of deal.data.materialsList) { const material = materials.find(material => material.id === tableItem.materialsList.id); const dealNeeds = <NeedByDeal> { dealId: deal.id, dealName: deal.data.__name, quantity: tableItem.materialAmount, }; if (!materialNeeds[tableItem.materialsList.id]) { materialNeeds[tableItem.materialsList.id] = <NeedForMaterials> { materialName: material!.data.__name, quantity: 0, deals: [], } } materialNeeds[tableItem.materialsList.id].quantity += tableItem.materialAmount; materialNeeds[tableItem.materialsList.id].deals.push(dealNeeds); } } }
Widget template
The Code widget is added to the page. It allows you to create any layout (including dynamic display of data) using HTML, CSS, and code embeddings. As an example, let’s display the ordered materials as a table. We need to create the table’s framework with two columns, Material and Amount. We’ll use the
<tbody></tbody>
container for the table’s body.Inside
tbody
, we need to iterate over the list of ordered materials we got using thegetNeeds()
function. For each item, we need to render a templaterenderNeed
. As the argument, we’ll pass the corresponding item from the list of ordered materials with the structure identical toNeedForMaterials
:<table> <thead> <tr> <th>Material</th> <th>Amount</th> </tr> </thead> <tbody> <% for (const need of getNeeds()) { %> <%= renderNeed(need) %> <% } %> </tbody> </table>
In the
renderNeed
template, let’s add a table row<tr class='material-need-deals'>
for each material. Intd
cells, we’ll add the name of a materialneed.materialName
and the total amount needed for all ordersneed.quantity
.Then, if there are orders for a material
need.deals
, we’ll add a row for each one (<tr class='material-need-deals'>
) using a loop. In its cells, we’ll specify the order’s namedeal.dealName
and the amount of material required to fulfil the orderdeal.quantity
.<% $template renderNeed(need) %> <tr class='material-need-title'> <td><%- need.materialName %></td> <td><%- need.quantity %></td> </tr> <% if (need.deals.length) { %> <% for (const need of getNeeds()) { %> <tr class='material-need-deals'> <td class='material-need-deals__name'><%- deal.dealName %></td> <td><%- deal.quantity %></td> </tr> <% } %> <% } %> <% $endtemplate %>
The page is almost ready. We only need to improve its design so that it presents the data clearly. To do that, we can add CSS styles to the beginning of the Code widget in the
<style> ... </style>
container. It will be applied to the page via tags and element classes. We can set a separate class for the table or wrap it inside a container with the necessary class.After applying styles and improving the layout, this is what the Code widget will look like:
<style> .needs-container table { width: 100%; } .needs-container thead { border-top: 1px solid #d9d9d9; border-bottom: 1px solid #d9d9d9; } .needs-container th, .needs-container td { padding: 18px 6px; } .needs-container th { color: #8c8c8c; } tr.material-need-title { font-weight: bold; background-color: #e3eef7; } tr.material-need-deals-title-row td { padding-top: 0px; padding-bottom: 0px; } td.material-need-deals__name { padding-left: 36px; } </style> <div class='needs-container'> <table> <thead> <tr> <th>Material</th> <th>Amount</th> </tr> </thead> <tbody> <% for (const need of getNeeds()) { %> <%= renderNeed(need) %> <% } %> </tbody> </table> </div> <% $template renderNeed(need) %> <tr class='material-need-title'> <td><%- need.materialName %></td> <td><%- need.quantity %></td> </tr> <% if (need.deals.length) { %> <% for (const deal of need.deals) { %> <tr class='material-need-deals'> <td class='material-need-deals__name'><%- deal.dealName %></td> <td><%- deal.quantity %></td> </tr> <% } %> <% } %> <% $endtemplate %>
This is what the page will look like when it’s rendered:
Dynamic display of data
Let’s enhance the report we’ve created by adding dynamic display of data. First, let’s make it possible to view the material or order by clicking its name in the table. To do that, we’ll wrap the names of materials and orders in links, so that we get a string of the following format: (p:item/workspace_code/app_code/item_ID):
<a href='(p:item/installation_orders/materials/<%- need.materialId %>)'> <%- need.materialName %> </a>
<a href='(p:item/installation_orders/deals/<%- deal.dealId %>)'> <%- deal.dealName %> </a>
Now let’s add a way to expand and collapse the list of orders associated with a material:
showDeals
property of the Boolean type to the interface we created for the ordered materials list. This property will determine whether orders associated with a material need to be displayed:interface NeedForMaterials { materialName: string; quantity: number; deals: NeedByDeal[]; showDeals: boolean; }
In the template used to render a list item, before the link to the material’s name, let’s add the plus and minus symbols. One or the other will be displayed next to a material’s name, depending on the value of
showDeals
of the corresponding item in the list. Let’s call thetoggleShowDeals
function upon clicking on the<span>
element that will be the button to expand and collapse the list. We need to pass the material’s ID to this function:onclick='<%= Scripts %>.toggleShowDeals('<%= need.materialId %>')'
. Note that when you call a function from scripts, you can only pass a String type property in theonclick
attribute, not the whole object. Otherwise, the<
and>
characters will be processed incorrectly.We also need to check the
need.showDeals
property in the condition that defines the visibility of the orders associated with a material:<% if (need.showDeals && need.deals.length) { %>
.The final version of the template used to render an item of the ordered materials list will look like this:
<% $template renderNeed(need) %> <tr class='material-need-title'> <td> <span class='show-deals-toggle' onclick='<%= Scripts %>.toggleShowDeals('<%= need.materialId %>')' > <% if (need.showDeals) { %> - <% } else { %> + <% } %> </span> <a href='(p:item/installation_orders/materials/<%- need.materialId %>)'> <%- need.materialName %> </a> </td> <td><%- need.quantity %></td> </tr> <% if (need.showDeals && need.deals.length) { %> <% for (const deal of need.deals) { %> <tr class='material-need-deals'> <td class='material-need-deals__name'> <a href='(p:item/installation_orders/deals/<%- deal.dealId %>)'> <%- deal.dealName %> </a> </td> <td><%- deal.quantity %></td> </tr> <% } %> <% } %> <% $endtemplate %>
toggleShowDeals
function that receives a material’s ID from the Code widget, uses it to search for a corresponding item of the ordered materials list in thematerialNeeds
array, and changes the value of theshowDeals
property to the opposite one:function toggleShowDeals (materialId: string) { const need = materialNeeds.find(need => need.materialId === materialId); if (!need) { return; } need.showDeals = !need.showDeals; }
When data in the table changes, the widget needs to be updated. Whether a widget needs to be re-calculated and re-rendered is checked whenever the value of a context variable the widget is linked with changes. We’ll use this to update the information. Let’s add the property we are going to change to the Context tab of the interface designer. It can be a property of the Date/time type named
timestamp
.Let’s use this property in the Code widget. It doesn’t matter how exactly it’s going to be used. For example, we can add an empty condition:
<% if (Context.data.timestamp) {} %>
In scripts, we’ll assign this property a new value whenever we need to render the widget again:
function toggleShowDeals (materialId: string) { const need = materialNeeds.find(need => need.materialId === materialId); if (!need) { return; } need.showDeals = !need.showDeals; Context.data.timestamp = new Datetime(); }
Now we can expand and collapse the list of orders associated with a material:
Add widgets in the Code widget
We can let users filter data in the table. To do that, let’s open the Context tab in the interface designer and add properties that will be used to enter filter conditions. They will only work correctly if their types correspond with the types of fields that rows need to be filtered by. Let’s create two properties,
materialsOrder
andcontractor
:To manage filters, let’s add the Pop-up widget (Widget.popover) that will open when a user clicks the corresponding button. You can learn more about adding this widget or other widgets in this article. To make it possible to work with filters in a pop-up window, let’s change the table’s header and add the Pop-up widget consisting of the window opening element
filtersOpener
, the window’s contentfiltersContent
, andUI.widget.popover( ... )
that adds the widget itself.Let’s add a header to the pop-up window’s content and insert the added View context properties using
UI.widget.viewContextRow( ... )
. Let’s also add a button that will call the function that processes filter conditions (fields by which the data is filtered),<%= Scripts %>.applyFilters()
:<thead> <tr> <th> Material <% $template filtersContent %> // Content of the pop-up window <h4> Filtering options: </h4> <%= UI.widget.viewContextRow('materialsOrder', { name: 'Order' }) %> <%= UI.widget.viewContextRow('contractor', { name: 'Client' }) %> <button type='button' class='btn btn-default' onclick='<%= Scripts %>.applyFilters()' > Apply </button> <% $endtemplate %> <% $template filtersOpener %> // Clicking on this container opens the pop-up <button type='button' class='btn btn-default btn-link'>Configure</button> <% $endtemplate %> // Adding the widget <%= UI.widget.popover( filtersOpener, { type: 'widgets', size: 'lg', position: 'right', }, filtersContent ) %> </th> <th>Amount</th> </tr> </thead>
Now let’s add this function to the client script:
async function applyFilters () { // Let’s get the current values of fields used as filter conditions const materialsOrder = Context.data.materialsOrder; const contractor = Context.data.contractor; // Updating data before applying filters await fillNeeds(); if (!materialsOrder && !contractor) { // If no filters are applied, the execution is stopped; all data will be displayed // Updating the table by assigning a new value to the property we created for updating Context.data.timestamp = new Datetime(); return; } // Creating an empty array to fill with data const filteredNeeds: NeedForMaterials[] = []; for (const need of materialNeeds) { // For each material, we need to reset the amount and create a new array of filtered orders need.quantity = 0; const filteredDeals: NeedByDeal[] = []; for (const deal of need.deals) { if ( (!materialsOrder || deal.dealId === materialsOrder.id) && (!contractor || contractor.id === deal.clientId)) { // If an order meets filter conditions, it is added to the list, // and the amount of the material required for it is entered filteredDeals.push(deal); need.quantity += deal.quantity; } } // Replacing the list of orders by the filtered orders need.deals = filteredDeals; if (need.quantity) { // If the amount of the material does not equal zero, it is added to the list filteredNeeds.push(need); } } // Updating the main list of ordered materials using the filtered values // and rendering the table again by assigning a new value to `timestamp` materialNeeds = filteredNeeds; Context.data.timestamp = new Datetime(); }
Let’s try to use the filters we created. When a user clicks the Configure button, a pop-up window with fields for filtering is opened near the table’s header. Let’s filter the data by the Client field, setting it to Client 2:
When the user clicks Apply, the filtering window closes, and the updated list only includes materials needed to fulfil orders issued by Client 2:
Let’s open the filtering window again and filter the data by the Order field, setting it to Order No. 2 from Client 2:
The updated table includes materials from Order No. 2 and orders issued by Client 2: