Skip to content
PDF

This guide introduces the basic functionalities related to the Module Suite Smart Pages.

Basic concepts

The Module Suite Smart Pages is an optional extension to Module Suite that introduces new features for those users that need an extra level of flexibility when creating customized SmartUI perspectives, and, more broadly, for those who prefer using the SmartUI in place of the Classic UI for their Content Server applications.

The extension includes the following components:

  • A new set of SmartUI tiles, available within the Perspective Builder Widget Library
  • A set of Content Script snippets that showcase how to create datasources for the SmartUI tiles
  • Smart Pages module. Smart Pages module that aims to simplify the creation of good-looking functional user interfaces, both as a standalone solution and as part of the Smart View perspectives.
  • Low-coding Smart View tailoring capabilities (allows you to custoize several aspect of the Smart View without having to relay on the Smart View SDK and without the need to deploy new artifact on Content Server servers)

Module Suite Tiles in the Widget Library

Widget Library

The following tiles are available in AnswerModules Module Suite section:

  • Content Script Result
  • Content Script Tile Chart
  • Content Script Tile Links
  • Content Script Tile Tree
  • Content Script Node Table
  • Content Script Tile News
  • Content Script Tile Tiles

Tile Configuration

Module Suite tiles share some common configuration options, while other options are specific to single tiles.

Common options include the configuration of the external frame (header, scrolling content, title, icon) and the configuration of the tile's Data Source. All Module Suite tiles require to specify a Content Script object that will be executed when the tile content is created. This script acts as a Data Source for the tile, and allows to make its content dynamic.

Through the configuration, it is also possible to pass additional parameters to the script. The parameter will be available to the developer within the params variable.

When configuring the tile's icon, two different approaches are possible:

  • specify a CSS style class to apply to the icon element. This should define the rules needed to apply the desired icon.
  • specify the name (and color scheme) of the desired icon among the ones available in the Module Suite icon set. See the icon reference cheat sheet for a full list of options.

Content Script Tile configuration

Tile: Content Script Result

The Content Script Result is a general-purpose tile that can be used to inject any output generated by a Content Script Data source into a SmartUI perspective.

Content Script Result

Tile: Content Script Tile Chart

The Content Script Tile Chart is a tile who's purpose is to create interactive charts within the SmartUI. The data shown in the charts will be provided by a Content Script data source.

Tile Chart

Chart tiles leverage two different javascript libraries:

  • Chartist (supported for backward compatibility)
  • Chart.js (suggested)

Depending on the selected chart type, the appropriate configuration has to be provided in JSON format. The following sample configuration produces the pie chart in the image above.

def rand = new Random()

if(params.widgetConfig){

    json(widgetConfig:[
        reloadCommands:["updateChart"],
        html:"""


<small>Move the mouse over the chart for triggering data-reload</small>
<script>
    csui.onReady2([ 
        "csui/lib/jquery", 
        "csui/lib/underscore", 
        "csui/lib/radio"], 
                  function(jQ, _, Radio){

                      //Get the page message bus
                      var amChannel  = Radio.channel('ampagenotify');

                      //Get the chart
                      var chart = amChannel.request("ampages:myChart");

                      var canvas = jQ("#myChart");
                      canvas.unbind("click");
                      canvas.on("click", function (evt) {
                        var activePoints = chart.getElementsAtEvent(evt);
                        var vals = _.map(_.pluck(_.filter(chart.legend.legendItems, function(it){ return it.hidden==false}), "text"), function(value){ return value;}).join("|");
                        if(!_.isUndefined(activePoints[0])){
                           var chartData = activePoints[0]['_chart'].config.data;
                           var idx = activePoints[0]['_index'];

                           var label = chartData.labels[idx];
                           var value = chartData.datasets[0].data[idx];     
                           amChannel.trigger("updateChart", [ {name:"where_type", value:label} ]);

                       } else {
                           amChannel.trigger("updateChart", [ {name:"where_type", value:vals} ]);
                       }
                      });


                      canvas.hover(function(){
                         var self = jQ(this);
                         //jQ(".myChartLoader").removeClass("binf-hidden");
                         amChannel.trigger("updateChart", [{name:"filter", value:"first"}]);
                      });
                  });
</script>"""
    ])
}else{

    json([

        type:"bar",
        data: 
        [

            labels: ["red", "green"],
            datasets: [

                [
                    label: "My First dataset",
                    backgroundColor: "${AMBWFWidgetsLib.getBehaviour("ambwf","generateRandomHTMLColor", this)(rand)}",
                    borderColor: "${AMBWFWidgetsLib.getBehaviour("ambwf","generateRandomHTMLColor", this)(rand)}",
                    data: [rand.nextInt(100), rand.nextInt(100)],

                ],
                [
                    label: "My Second dataset",
                    borderColor: "${AMBWFWidgetsLib.getBehaviour("ambwf","generateRandomHTMLColor", this)(rand)}",
                    backgroundColor: "${AMBWFWidgetsLib.getBehaviour("ambwf","generateRandomHTMLColor", this)(rand)}",
                    data: [ rand.nextInt(100), rand.nextInt(100)],

                ]
            ]
        ],
        options: [
            maintainAspectRatio: false,
            title: [
                display: true,
                text: 'myChart',
                position: 'left'
            ],
            legend: [
                display: true,
                position: 'top'
            ],
            scales: [
                yAxes: [
                    [
                        ticks: [
                            beginAtZero:true
                        ]
                    ]
                ]
            ]
        ]
    ])
}

Tile: Content Script Tile Tiles

The Content Script Tile Tiles is a tile meant to create a customizable list of clickable links and HTML Tiles. The data controlling the links is provided by the backing Content Script data source.

Tile Links

The following Content Script sample configuration produces the Links tile shown above.

def targetSpaceFilter = 2000


def subtypeFilter = "144".split(",") 

if(params.widgetConfig){
    json([
      widgetConfig:[
          reloadCommands:[ "updateData" ],
          columnsWithSearch:[ "Owner", "Name" ]
      ]
     ])
    return 
}

if(params.page?.contains("_") && params.page_list){
    if(params.page_list[0].contains("_") && !params.page_list?[1]?.contains("_")){
        params.page = params.page_list[1]
    }else if(!params.page_list[0].contains("_") && params.page_list?[1]?.contains("_")){
        params.page = params.page_list[0]
    }
}

def paging = [actual_count:0, 
              limit:((params.limit?:"30") as int), 
              page:((params.page?:"1") as int), 
              page_total:0, 
              range_max:0, 
              range_min:0, 
              total_count:0, 
              total_row_count:0, 
              total_source_count:0]

def pageSize = paging.limit
def offset = (paging.limit * (paging.page - 1))
def firstRow = offset + 1
def lastRow = firstRow + paging.limit

nodes = []

def nameFilter = null
if( params.where_name ){
     nameFilter = "%${params.where_name}%"
}

def ownerFilter = null
if( params.where_owner ){
     ownerFilter = "%${params.where_owner}%"
}

def sortingOrderParam     = 'desc'
def sortingColumnParam     = 'name'

def sortingOrder     = 'DESC'
def sortingColumn     = 'DTree.Name'


if( params.sort && params.sort.contains('_') ){

    def sorting = params.sort.split('_')

    sortingOrderParam     = sorting[0]
    sortingColumnParam     = sorting[1]

    sortingOrder = ( sortingOrderParam == 'asc' ) ? 'ASC' : 'DESC'


    switch( sortingColumnParam?.trim() ){

        case 'name' :
            sortingColumn = 'DTree.Name'
            break

        case 'owner' :
            sortingColumn = 'KUAF.ID'
            break

        default :
            sortingColumn = 'DTree.Name'
            break
    }

}


try{


    def queryParams = [targetSpaceFilter as String]
    def queryIndex = 1

    def permExpr = "(exists (select DataID from DTreeACL aclT where aclT.DataID=DTree.DataID and ${users.getRightsStringForSQL("RightID", false)} and See >1 ))"


    sqlCode = """ select DTree.DataID "DID", 
                         DTree.Name "NAME", 
                         COUNT(*) OVER() as "overall_count" 

                    from DTree 
                    LEFT JOIN KUAF ON DTree.UserID = KUAF.ID

                    where DTree.ParentID = %1 """

    if(subtypeFilter.size() == 1){
        sqlCode += " and DTree.SubType = %${++queryIndex} "
        queryParams << (subtypeFilter[0] as long)
    } else if( subtypeFilter.size() > 1 ) {
        sqlCode += " and DTree.SubType IN (${subtypeFilter.join(',')}) "
    }

    if(nameFilter){
        sqlCode += " and DTree.Name LIKE %${++queryIndex} "
        queryParams << (nameFilter as String)
    }

    if(ownerFilter){
        sqlCode += " and (KUAF.Name LIKE %${++queryIndex} OR KUAF.LastName LIKE %${queryIndex} ) "
        queryParams << (ownerFilter as String)
    }

    if(!users.current.canAdministerSystem){
        sqlCode += " and ${permExpr} "
    }

    sqlCode += """
                   ORDER BY ${sortingColumn} ${sortingOrder}
                    OFFSET ${offset} ROWS
                    FETCH NEXT ${pageSize} ROWS ONLY

                """



    def queryResults

    if(queryParams){
        queryResults =  sql.runSQLFast(sqlCode, true, true, 100, *queryParams).rows
    } else {
        queryResults =  sql.runSQLFast(sqlCode, true, true, 100 ).rows
    }   

    def totalCount = (queryResults) ? queryResults[0].overall_count : 0

    nodes = queryResults?.collect{it.DID as Long}



    paging << [
              actual_count:totalCount, 
              page_total:((totalCount%paging.limit)+1),
              range_min:paging.page*paging.limit-paging.limit+1,
              range_max:(paging.limit*(paging.page+1)-totalCount)>0?(paging.limit*(paging.page+1)-totalCount):paging.limit*(paging.page+1),
              total_count:totalCount, 
              total_row_count:totalCount, 
              total_source_count:totalCount]

}catch(e){
    log.error("Error loading nodes table data",e)
    printError(e)
}



def drawStatusBar = { node ->

    def statusList = ['Draft', 'Under Revision', 'Approved', 'Published']
    def numSteps = statusList.size()
    def currStep = new Random().nextInt(statusList.size())
    def currStepName = statusList[currStep]


    def stepStyle = "height:100%; width:calc(100% / ${numSteps}); float:left; background-color:#F0AD4E; box-sizing:border-box;"

    def stepsHtml = ""

    (currStep + 1).times{
        stepsHtml += """<span style="${stepStyle}"></span>"""
    }


    return """ 
    <div style="text-align:center; font-size:.75em">${currStepName}</div>
    <div style="margin:3px 0; padding:0; height:5px; background-color:#eee;">${stepsHtml}</div>"""
}



def slurper = new JsonSlurper()

def processNode = { node, myNode ->

    /* Add your custom node post-processing here */

    //def myNode = asCSNode(node?.data.properties.id as long)

    node.data.amcsproxy =  [
        columns: [:],
        commands:[] 
    ]

    //Add custom column: node.data.amcsproxy.colums.sample_column = "My custom Value"

        def owner = myNode.createdBy
        def ownerBox = "<span><img src='/otcs/cs.exe/pulse/photos/userphoto/${owner.ID}/2000' style='max-height: 3em; border-radius: 50%; margin-right: 5px; vertical-align: middle;' /> ${owner.displayName}</span>"
        node.data.amcsproxy.columns.owner = ownerBox

    node.data.amcsproxy.columns.comment = myNode.comment 
     node.data.amcsproxy.columns.statusBar = drawStatusBar( myNode ) 

    return node
}

results = []

def fields = JsonOutput.toJson( [
    'actions': [ 'fields': [] ],
    'properties': [ 'fields': [] ],
    'versions': [ 'fields': [] ],
    'amcsproxy': [ 'fields': [] ],
])

//Identifies actions to be displayed for every node
//Node actions are return together with data request thay may lead to additinal response time
// [] - docman.getNodesRestV2JSon will not process actions. 
//      Actions will be processed on a separate call based on the list provided (see returned json object at the end of this script)
// null - default list of actions will be returned
// ['open','properties','copy','move','edit'] - sample list of actions
// To ideal actions processing requires you to assign an empty list (see below) to the nodesActions variable below and pass the list of commands to be retrived
// using the 'actions' list property of the json object returned by this script (see last line)
def nodesActions = [] 

if( nodes.size() > 1 ){
    log.error("Nodes ${nodes}")
    temp = slurper.parseText( docman.getNodesRestV2JSon(nodes, fields, '{"properties":{"fields":["parent_id"]}}', false, false, nodesActions) )
    theNodes = docman.getNodesFastWith(nodes, [], params, false, false, false)
    nodes.each{ node ->

        def jsonNode = temp.find{ it.data.properties.id == node }
        results << processNode(jsonNode, theNodes.find{it.ID == node}  )
    }


} else if (nodes.size() == 1 ){

    it = slurper.parseText(docman.getNodesRestV2JSon(nodes, fields, '{"properties":{"fields":["parent_id"]}}', false, false, nodesActions))
    processNode(it, docman.getNodeFast(nodes[0])) 

    results = [it]
}




def columns = [

    type: [
            key:"type",
            name:"Type",
            type:2,
            type_name:"Integer",
            sort:false
          ]

    ,name: [
            key:"name",
            name:"Name",
            type:-1,
            type_name:"String",
            sort:true,
            align:"left"
          ]

    ,owner: [
            key:"owner",
            name:"Owner",
            type:43200,
            type_name:"String",
            sort:true,
            align:"left"
          ]

    ,statusBar: [
            key:"statusBar",
            name:"Doc. Status",
            type:43200,
            type_name:"String",
            sort:false,
            align:"left"
          ]

    ,comment: [
            key:"comment",
            name:"Comment",
            type:-1,
            type_name:"String",
            sort:false,
            align:"left"
          ]
]


// actions - list of commands defined for all the nodes listed in the page
// action=[] - will return all possible actions for a node
json(
    [
      paging:paging,
      columnsWithSearch:[ "name" , "owner" ], 
      results:results, 
      columns:columns, 
      tableColumns:columns,
      widgetConfig:[
          reloadCommands:[ "updateData" ]
      ],
      actions: ['open','properties','copy']
   ]
)

The Content Script Tile Links is a tile meant to create a customizable list of clickable links. The data controlling the links is provided by the backing Content Script data source.

Tile Links

The following Content Script sample configuration produces the Links tile shown above.

if(params.widgetConfig){

    json(widgetConfig:[
        reloadCommands:["updateLinks"],
        html:"""

<style>
div.ans-tile-content-linkstiles{
    background: linear-gradient(180deg, #122c69 0%, #078db3 100% );
    color:#fff;
    height:100%;
}

div.ans-tile-content-linkstiles > div.binf-list-group > a:nth-child(2),
div.ans-tile-content-linkstiles > div.binf-list-group > a:nth-child(6),
div.ans-tile-content-linkstiles > div.binf-list-group > a:nth-child(10){
    background:#00639b;
    color:#fff;
    border-radius:0px;
}
div.ans-tile-content-linkstiles > div.binf-list-group > a:nth-child(3),
div.ans-tile-content-linkstiles > div.binf-list-group > a:nth-child(7),
div.ans-tile-content-linkstiles > div.binf-list-group > a:nth-child(11){
    background:#df3324;
    color:#fff;
    border-radius:0px;
}
div.ans-tile-content-linkstiles > div.binf-list-group > a:nth-child(4),
div.ans-tile-content-linkstiles > div.binf-list-group > a:nth-child(8),
div.ans-tile-content-linkstiles > div.binf-list-group > a:nth-child(12){
    background:#008485;
    color:#fff;
    border-radius:0px;
}

</style>
<div style="padding:20px; background-color:white;margin-bottom:10px;color:#333" >
Click on the differnt links to see them in action.
</div>
<script>
    csui.onReady2([    'csui/lib/underscore',
        'csui/lib/backbone',
        'csui/lib/jquery',
        'csui/lib/radio'], 
        function(_,Backbone, jQ, Radio){
            var amChannel = Radio.channel("ampagenotify");
            amChannel.on("smartPage_action", function(action,param){
                console.log("GOT Page Action request. Action: "+action+ " parameter: "+param);
            });

        });
</script>
"""
    ])
}else{

    retVal = 
        [
            data:[
                links:[
                    [
                        issection:true,
                        name:"First Section",
                    ],
                    [
                        issection:false,
                        icon:"csui-icon-home",
                        name:"First Link (Navigate)",
                        desc:"More information for this link",

                        url:"#", //If action != null url must be set equal to #
                        action:"navigate", //Will trigger a browse action of the current view
                        params:"2000", //The DataID of the node you wanto to navigate to 

                    ],
                    [
                        issection:false,
                        icon:"icon-tileExpand icon-perspective-open",
                        name:"Duplicate (Action)",

                        url:"#", //If action != null url must be set equal to #
                        action:"notify", //Will trigger the execution of the command below
                        command:"updateLinks", //The action to execute
                        params:"duplicate", //The action's parameter, this value will be passed to the script in a parameter named "tile"

                    ],
                    [
                        issection:false,
                        icon:"icon-socialFavOpen",
                        name:"Notify Smart Page (Page Action)",

                        url:"#", //If action != null url must be set equal to #
                        command:"smartPage", //The SmartPage(s) to notify
                        action:"updatePage",   //The action to execute
                        params:"2000" //The action's parameter

                    ],
                    [
                        issection:false,
                        am_icon:"am_icon_link",
                        am_icon_schema:"am_icon_green",
                        name:"Simple link",

                        url:"http://www.answermodules.com",
                        newtab:true

                    ]

                ]
            ]
        ]

    if(params.tile == "duplicate"){
        retVal.data.links += retVal.data.links[-5].clone()
        retVal.data.links += retVal.data.links[-5]
        retVal.data.links += retVal.data.links[-5]
        retVal.data.links += retVal.data.links[-5]

        retVal.data.links[-4].name = "Second Section"
    }else if(params.tile == "triple"){
        retVal.data.links += retVal.data.links[-5].clone()
        retVal.data.links += retVal.data.links[-5]
        retVal.data.links += retVal.data.links[-5]
        retVal.data.links += retVal.data.links[-5]

        retVal.data.links[-4].name = "Second Section"

        retVal.data.links += retVal.data.links[-4].clone()
        retVal.data.links += retVal.data.links[-4]
        retVal.data.links += retVal.data.links[-4]
        retVal.data.links += retVal.data.links[-4]

        retVal.data.links[-4].name = "Third Section"
    }

    json(
        retVal
    )


}

Tile: Content Script Tile Tree

The Content Script Tile Tree creates an interactive tree structure with nodes that can be expanded and collapsed. The tree structure uses a Content Script data source for the initial data and for subsequent ajax data load calls.

Tile Tree

The following sample configuration generates the tree in the image above.

if(params.widgetConfig){
    json( [ id           : 2,
           widgetConfig : [
               tileLayoutClasses  : "", 
               tileContentClasses : "", 
               reloadCommands     : ["updateTree"],
               root               : 2000, 
               plugins            : [ "wholerow" ],
               theme              : [ 'name': 'proton',
                                     'responsive': true ],
               html               : """
<style>
div.ans-tile-tree{
  background: linear-gradient(180deg, #122c69 0%, #078db3 100% );
  color:#fff;
  height:calc(100vh - 222px);
  font-size:13px !important;
}
.binf-widgets .jstree-proton .jstree-icon.csui-icon-node-task {
  background-image:url('${img}csui/themes/carbonfiber/image/icons/mime_task.svg')
}
.binf-widgets .jstree-proton .jstree-icon.mime_pdf{
  background-image:url('${img}csui/themes/carbonfiber/image/icons/mime_pdf.svg')
}
.jstree-anchor small{
  font-size:.9em;
  font-style:italic;
}
</style>
<div class="am-form-text-input" style="margin-top: 1px;padding: 5px 0px;">
    <label class=" control-label col-form-label am-form-text-input-label   am-form-label-top" style="padding: 5px;">Filter tree</label>
    <div class="am-form-input-wrap" style="padding: 0 5px;">
        <input id="filter" type="text" placeholder="" class="form-control" style="border-radius: 0px;box-shadow: none;">
    </div>
</div>

<script>
    csui.onReady2([    'csui/lib/underscore',
        'csui/lib/backbone',
        'csui/lib/jquery',
        'csui/lib/radio'], 
        function(_,Backbone, jQ, Radio){
            var amChannel = Radio.channel("ampagenotify");
            amChannel.on("printConsole", function(params){
                console.log("GOT request "+JSON.stringify(params));
            });
            amChannel.on("smartPage_action", function(action,param){
                console.log("GOT Page Action request. Action: "+action+ " parameter: "+param);
            });
            jQ("#filter").on("blur", function(){
                amChannel.trigger("updateTree",{'term':jQ(this).val()})
            })  

        });
</script>"""
           ]
          ] )
    return
}

data =

    [
        [

            icon     : "csui-icon cs_vfolder", //mime_folder, cs_folder_root, cs_vfolder, cs_folder_open...
            id       : 1,
            text     : "Roots",
            children :  [
                [
                    action   : "navigate", //Trigger a Smart View navigation
                    icon     : "csui-icon cs_folder_root", //cs_folder_root, cs_vfolder, cs_folder_open
                    id       : 2000, //The node will be used as the action's parameter
                    text     : "Home",
                    children : false
                ]

            ],
            state       : [
                opened    : true
            ] 
        ],
        [

            action   : "printConsole", //Trigger a Tile action
            params   : "3", //This value will be passed to the script in a parameter named 'tile'
            icon     : "csui-icon mime_folder",
            id       : 3,
            text     : "Folder (Lazy Loaded)",
            children : true,
            state       : [
                opened    : false
            ] 
        ]
    ]


if(params.uiParentID == "3"){
    data[1].children = [
        [

            icon     : "csui-icon mime_folder",
            id       : 4,
            text     : "Sub Folder",
            children : [
                [
                    notify   : "smartPage",    //Triggers a Smart Page action noifying the provided page(s) (CSV)
                    action   : "customAction", //The action to execute
                    params   : "2000",         //The action's parameter
                    icon     : "csui-icon mime_pdf",
                    id       : 5,
                    text     : "Notify Smart Page",
                    children : false
                ],
                [
                    action   : "printConsole",
                    params   : "2000",
                    icon     : "csui-icon mime_pdf",
                    id       : 6,
                    text     : "Execute Action",
                    children : false
                ]

            ]
        ]

    ]
}
if(params.term){
    data = data.findAll{it.text.startsWith(params.term)}
}
json(data)

Tile: Content Script Node Table

The Content Script Node Table is an enhancement of the standard Node Table tile. The tile uses a Content Script as data source, allowing to set up any custom business logic to generate the list of nodes to be shown.

Tile Node Table

def targetSpaceFilter = 2000


def subtypeFilter = "144".split(",") 

if(params.widgetConfig){
    json([
      widgetConfig:[
          reloadCommands:[ "updateData" ],
          columnsWithSearch:[ "Owner", "Name" ]
      ]
     ])
    return 
}

if(params.page?.contains("_") && params.page_list){
    if(params.page_list[0].contains("_") && !params.page_list?[1]?.contains("_")){
        params.page = params.page_list[1]
    }else if(!params.page_list[0].contains("_") && params.page_list?[1]?.contains("_")){
        params.page = params.page_list[0]
    }
}

def paging = [actual_count:0, 
              limit:((params.limit?:"30") as int), 
              page:((params.page?:"1") as int), 
              page_total:0, 
              range_max:0, 
              range_min:0, 
              total_count:0, 
              total_row_count:0, 
              total_source_count:0]

def pageSize = paging.limit
def offset = (paging.limit * (paging.page - 1))
def firstRow = offset + 1
def lastRow = firstRow + paging.limit

nodes = []

def nameFilter = null
if( params.where_name ){
     nameFilter = "%${params.where_name}%"
}

def ownerFilter = null
if( params.where_owner ){
     ownerFilter = "%${params.where_owner}%"
}

def sortingOrderParam     = 'desc'
def sortingColumnParam     = 'name'

def sortingOrder     = 'DESC'
def sortingColumn     = 'DTree.Name'


if( params.sort && params.sort.contains('_') ){

    def sorting = params.sort.split('_')

    sortingOrderParam     = sorting[0]
    sortingColumnParam     = sorting[1]

    sortingOrder = ( sortingOrderParam == 'asc' ) ? 'ASC' : 'DESC'


    switch( sortingColumnParam?.trim() ){

        case 'name' :
            sortingColumn = 'DTree.Name'
            break

        case 'owner' :
            sortingColumn = 'KUAF.ID'
            break

        default :
            sortingColumn = 'DTree.Name'
            break
    }

}


try{


    def queryParams = [targetSpaceFilter as String]
    def queryIndex = 1

    def permExpr = "(exists (select DataID from DTreeACL aclT where aclT.DataID=DTree.DataID and ${users.getRightsStringForSQL("RightID", false)} and See >1 ))"


    sqlCode = """ select DTree.DataID "DID", 
                         DTree.Name "NAME", 
                         COUNT(*) OVER() as "overall_count" 

                    from DTree 
                    LEFT JOIN KUAF ON DTree.UserID = KUAF.ID

                    where DTree.ParentID = %1 """

    if(subtypeFilter.size() == 1){
        sqlCode += " and DTree.SubType = %${++queryIndex} "
        queryParams << (subtypeFilter[0] as long)
    } else if( subtypeFilter.size() > 1 ) {
        sqlCode += " and DTree.SubType IN (${subtypeFilter.join(',')}) "
    }

    if(nameFilter){
        sqlCode += " and DTree.Name LIKE %${++queryIndex} "
        queryParams << (nameFilter as String)
    }

    if(ownerFilter){
        sqlCode += " and (KUAF.Name LIKE %${++queryIndex} OR KUAF.LastName LIKE %${queryIndex} ) "
        queryParams << (ownerFilter as String)
    }

    if(!users.current.canAdministerSystem){
        sqlCode += " and ${permExpr} "
    }

    sqlCode += """
                   ORDER BY ${sortingColumn} ${sortingOrder}
                    OFFSET ${offset} ROWS
                    FETCH NEXT ${pageSize} ROWS ONLY

                """



    def queryResults

    if(queryParams){
        queryResults =  sql.runSQLFast(sqlCode, true, true, 100, *queryParams).rows
    } else {
        queryResults =  sql.runSQLFast(sqlCode, true, true, 100 ).rows
    }   

    def totalCount = (queryResults) ? queryResults[0].overall_count : 0

    nodes = queryResults?.collect{it.DID as Long}



    paging << [
              actual_count:totalCount, 
              page_total:((totalCount%paging.limit)+1),
              range_min:paging.page*paging.limit-paging.limit+1,
              range_max:(paging.limit*(paging.page+1)-totalCount)>0?(paging.limit*(paging.page+1)-totalCount):paging.limit*(paging.page+1),
              total_count:totalCount, 
              total_row_count:totalCount, 
              total_source_count:totalCount]

}catch(e){
    log.error("Error loading nodes table data",e)
    printError(e)
}



def drawStatusBar = { node ->

    def statusList = ['Draft', 'Under Revision', 'Approved', 'Published']
    def numSteps = statusList.size()
    def currStep = new Random().nextInt(statusList.size())
    def currStepName = statusList[currStep]


    def stepStyle = "height:100%; width:calc(100% / ${numSteps}); float:left; background-color:#F0AD4E; box-sizing:border-box;"

    def stepsHtml = ""

    (currStep + 1).times{
        stepsHtml += """<span style="${stepStyle}"></span>"""
    }


    return """ 
    <div style="text-align:center; font-size:.75em">${currStepName}</div>
    <div style="margin:3px 0; padding:0; height:5px; background-color:#eee;">${stepsHtml}</div>"""
}



def slurper = new JsonSlurper()

def processNode = { node, myNode ->

    /* Add your custom node post-processing here */

    //def myNode = asCSNode(node?.data.properties.id as long)

    node.data.amcsproxy =  [
        columns: [:],
        commands:[] 
    ]

    //Add custom column: node.data.amcsproxy.colums.sample_column = "My custom Value"

        def owner = myNode.createdBy
        def ownerBox = "<span><img src='/otcs/cs.exe/pulse/photos/userphoto/${owner.ID}/2000' style='max-height: 3em; border-radius: 50%; margin-right: 5px; vertical-align: middle;' /> ${owner.displayName}</span>"
        node.data.amcsproxy.columns.owner = ownerBox

    node.data.amcsproxy.columns.comment = myNode.comment 
     node.data.amcsproxy.columns.statusBar = drawStatusBar( myNode ) 

    return node
}

results = []

def fields = JsonOutput.toJson( [
    'actions': [ 'fields': [] ],
    'properties': [ 'fields': [] ],
    'versions': [ 'fields': [] ],
    'amcsproxy': [ 'fields': [] ],
])

//Identifies actions to be displayed for every node
//Node actions are return together with data request thay may lead to additinal response time
// [] - docman.getNodesRestV2JSon will not process actions. 
//      Actions will be processed on a separate call based on the list provided (see returned json object at the end of this script)
// null - default list of actions will be returned
// ['open','properties','copy','move','edit'] - sample list of actions
// To ideal actions processing requires you to assign an empty list (see below) to the nodesActions variable below and pass the list of commands to be retrived
// using the 'actions' list property of the json object returned by this script (see last line)
def nodesActions = [] 

if( nodes.size() > 1 ){
    log.error("Nodes ${nodes}")
    temp = slurper.parseText( docman.getNodesRestV2JSon(nodes, fields, '{"properties":{"fields":["parent_id"]}}', false, false, nodesActions) )
    theNodes = docman.getNodesFastWith(nodes, [], params, false, false, false)
    nodes.each{ node ->

        def jsonNode = temp.find{ it.data.properties.id == node }
        results << processNode(jsonNode, theNodes.find{it.ID == node}  )
    }


} else if (nodes.size() == 1 ){

    it = slurper.parseText(docman.getNodesRestV2JSon(nodes, fields, '{"properties":{"fields":["parent_id"]}}', false, false, nodesActions))
    processNode(it, docman.getNodeFast(nodes[0])) 

    results = [it]
}




def columns = [

    type: [
            key:"type",
            name:"Type",
            type:2,
            type_name:"Integer",
            sort:false
          ]

    ,name: [
            key:"name",
            name:"Name",
            type:-1,
            type_name:"String",
            sort:true,
            align:"left"
          ]

    ,owner: [
            key:"owner",
            name:"Owner",
            type:43200,
            type_name:"String",
            sort:true,
            align:"left"
          ]

    ,statusBar: [
            key:"statusBar",
            name:"Doc. Status",
            type:43200,
            type_name:"String",
            sort:false,
            align:"left"
          ]

    ,comment: [
            key:"comment",
            name:"Comment",
            type:-1,
            type_name:"String",
            sort:false,
            align:"left"
          ]
]


// actions - list of commands defined for all the nodes listed in the page
// action=[] - will return all possible actions for a node
json(
    [
      paging:paging,
      columnsWithSearch:[ "name" , "owner" ], 
      results:results, 
      columns:columns, 
      tableColumns:columns,
      widgetConfig:[
          reloadCommands:[ "updateData" ]
      ],
      actions: ['open','properties','copy']
   ]
)

Embedding Beautiful WebForms views in SmartUI

In order to embed a Beautiful WebForms form in a SmartUI tile, it is possible to use a Content Script Result Tile with the following minimal configuration:

def formID = 123456 // the dataID of the form to embed
def viewID = 234567 // the dataID of the SmartUI form view, within the Form Template

form = forms.getFormInfo(formID)
view = asCSNode(viewID)

json([ output : view.renderView(binding, form),     
       widgetConfig :[ 
           reloadCommands:[], // any SmartUI commands that will trigger a reload of the form
           tileContentClasses:"am-nobckg",
           tileLayoutClasses:"am-nobckg"
       ]
     ])   

Form View Template

In order for the form to load resources compatible with usage within the SmartUI, you should use the "SmartView Embeddable" form template, available within the SmartUI extension libraries.

For additional details, see the dedicated section in the Beautiful WebForms documentation.

Icon reference cheat sheet

Iconset Color codes

Module Suite icons are available in the following colors:

Icons colors

All icons

A complete list of the currently available icons is shown below: Icons reference

Smart Pages

Smart View overrides - general concepts

Like many other features in Module Suite, Smart Pages overrides of Smart View features follow a convention on the configuration approach so that for applying a customization to the Smart View UI using one of the supported overrides it is sufficient, in most cases, to create the appropriate script under the appropriate Content Script Volume folder. Smart View overrides are organized as follows:

  • Content Script Volume
  • CSSmartView
    • Actions Used to define lazy loaded actions to be displayed in nodes' related actionbars
    • Commands Used o define new commands to be displayed in nodes' related actionbars
    • Columns Used to define custom dynamic columns to be displayed in Content Server spaces
    • Overrides Ovverrides configuration. Its content determines when and where a particular override (above) is used

Having a possible serious impact on the end user experience, it is important that the system is effective in calculating how, where and when overrides should be applied. For this reason Module Suite uses an elaborate algorithm to determine the Actual Override Map (AOP) to use when overrides should be applied. The following is a detailed description of how the AOM is determined.

The content of the Overrides folder is used to compute an Override Map (OM), specific to your repository, having the following structure:

OM = [
    "globals": [            (1)
        540588              
    ],
    "type": [               (2)
        "144": [            (3)
            548066
        ]
    ],
    "tenants": [            (4)
        "497147": [         (5)
            "globals": [    (6)
                548169
            ],
            "type": [       (7)
                "144": [    (8)
                    496932
                ]
            ],
            "ids": [        (9)
                "496931": [ (10)
                    545972
                ]
            ]
        ]
    ]
]

where:

  • (1) identifies a list of scripts to be always executed
  • (2) a list of scripts to be executed only if the current space has at least one node having of the identified type (3)
  • (4) scripts to be considered only if the current space is descendant of the specified tenant (5) (a space identified by its DataID)
  • (5) is a "tenant" configuration
  • (6) identifies a list of scripts that must always be executed if the current space is descendant of the specified tenant (5)
  • (7) a list of scripts to be executed only if the current space has at least one node having of the identified type (8) and is descendant of the specified tenant (5)
  • (9) a list of scripts to be executed only if the current space has at least one node having of the identified id (10) and is descendant of the specified tenant (5)
  • scripts in the OM are executed in the following order (1), (2), (6), (7), (10).

Given the above example and imagining that all the scripts in (3) (8) and (10) return the list ["comm_one","comm_two"], the resulting AOM will contain:

(3) AOM = [
                ...
                "S144":[commands:["comm_one","comm_two"]],
                ...
            ]
    (8) AOM = [
                ...
                "S144":[commands:["comm_one","comm_two"]],
                ...
            ]
    (10) AOM = [
                ...
                "D496931":[commands:["comm_one","comm_two"]],
                ...
            ]
- scripts in (1), (6), (10) MUST return a Map having entries of the form:
    "SXXXX":[                                                                      
        commands:["comm_one", "comm_two",...],
        columns: [ //Optional                                                       
                    col_name:"col value", //value can be HTML
                    ...
                    ]
    ]
    where XXXX is a valid SubType
    or
    "DYYYY":[                                                                      
        commands:["comm_one", "comm_two",...],
        columns: [  //Optional                                                    
                    col_name:"col value", //value can be HTML
                    ...
                    ]
    ]

where YYYY is a valid node's ID.

OM is to be considered a "static" information in productive environments and as such, to guarantee optimal performances, the framework should be allowed to cached it.

This can be done by setting to "true" the " amcs.amsui.volumeCache" parameter int the base configuration.

When a user changes the current space, the OM is evaluated by the framework against the users' permissions and the actual override map (AOM) associated to the space is determined. AOM is determined by executing the relevant scripts in OM in the order described above. The AOM has the following form:

AOM = [
    "S144":[                                                                       (1)
            commands:["comm_one", "comm_two",...], //list of commands' command_key  (2)
            columns: [                                                             (3)
                        col_name:"col value", //value can be HTML
                        ...
                     ]
           ],
    "D1234":[                                                                      (4)
            commands:["comm_one", "comm_two",...], //list of commands' command_key
            columns: [
                        col_name:"col value", //value can be HTML
                        ...
                     ]
           ]
    ...
]

where: (1) represents commands and columns to be associated to all the nodes having the identified subtype, (3) can be omitted, (4) represents commands and columns to be associated a specific node (identified by its id), (4) takes precedence over (1).

How OM is created ?

In order to determine the OM, the content of the Overrides folder is evaluated following the logic below:

[
    "globals":[             (1)     
        540588
    ],
    "type": [               (2)
        "144": [            (3)
            548066          
        ]
    ],
    "tenants": [            (4)
        "497147": [         (5)
            "globals": [    (6)
                548169
            ],
            "type": [       (7)
                "144": [    (8)
                    496932
                ]
            },
            "ids": [        (9)
                "496931": [ (10)
                    545972
                ]
            ]
        ]
    ]
]
  • (1) Contains the list of scripts objects stored directly under "Overrides"
  • (2) For each direct subfolder of "Overrides" that has a name starting by the letter "S" an entry is created in "type" map (2). The key of such entry is the target subtype (as specified in the subfolder's name) while the value is the list of scripts contained the aforementioned subfolder.
  • (4) For each direct subfolder of "Overrides" that has a name starting by the letter "D" an entry is created in "tenants" map (2). The key of such entry is the tenant's DataID (as specified in the subfolder's name) while the value is the tenant OM configuration.
  • (5) For each "tenant" subfolder a sub-Override Map is created (SOM). The structure of SOM is identical to the one of OM with the only difference that subfolders of a tenant subfolder having a name starting with the letter "D" are used in SOM for creating entries in the "ids" map.

Below an exemplar content of the Overrides folder

Name ID SubType
Overrides 00001 AnsTemplateFolder
- GlobaScript 00002 Content Script
- S144 00003 Content AnsTemplateFolder
- - Document Script 00004 Content Script
- D1234 00005 AnsTemplateFolder
- - S0 00006 AnsTemplateFolder
- - - Folder Script 00007 Content Script
- - D5678 00008 AnsTemplateFolder
- - - Node Script 00009 Content Script

and the resulting OM

[
    "globals":[                     
        00002
    ],
    "type": [               
        "144": [            
            00004          
        ]
    ],
    "tenants": [           
        "1234": [         
            "globals": [ ],
            "type": [       
                "0": [
                    00007
                ]
            },
            "ids": [
                "5678": [
                    00009
                ]
            ]
        ]
    }
]

Overrides

CSSmartView:Columns

It's possible to add/remove columns from/to browsing views using Content Scripts stored in the aforementioned folder. E.g.

example 1

//In the execution context of this script:
// - nodesColumns ( a map that associates nodes' ids with their columns definitions). Tipically contains a single entry
// - nodes: the list of nodes records. Tipically contains a single item.
// - req: the original REST request record
// - envelope: the current REST API call envelope

nodesColumns[3156087]?.add([type:43200, data_type:43200, name:"Add. Information", sort_key:"type", key:"_am_info"])

//Must return the revised nodeColumns
return nodesColumns

It is possible to enhance the information associated with nodes with column information injected via Module Suite E.g.

example 4

def drawStatusBar = { node ->

    def statusList = ['Draft', 'Under Revision', 'Approved', 'Published']
    def numSteps = statusList.size()
    def currStep = new Random().nextInt(statusList.size())
    def currStepName = node.name

    def stepStyle = "height:100%; width:calc(100% / ${numSteps}); float:left; background-color:#F0AD4E; box-sizing:border-box;"

    def stepsHtml = ""

    (currStep + 1).times{
        stepsHtml += """<span style="${stepStyle}"></span>"""
    }


    return """ 
    <div style="text-align:center; font-size:.75em">${currStepName}</div>
    <div style="margin:3px 0; padding:0; height:5px; background-color:#eee;">${stepsHtml}</div>""".toString()
}



retVal =  nodes.collect{
    [
        ("D${it.dataid}" as String):[ //The object returned MUST be made of simple types (no GString allowed)
            commands:["am_group", "am_bwf"],
            columns:[
                // Column defined in CSSmartView:Columns as nodesColumns[3156087]?.add([type:43200, data_type:43200, name:"Add. Information", sort_key:"type", key:"_am_info"])
                // columns of type 43200 can be used to inject HTML
                _am_info:drawStatusBar(it)
            ]
        ]
    ]
}
return retVal

CSSmartView:Actions

It's now possible to add custom actions to a node's menu lazy loaded set of actions . E.g.

example 2

/**
This script receives the following variables in the execution context:

- actions: a map that associates the node id to the list of available actions
E.g.
    "12345": {
        "data": {
            "Classify": {
                "content_type": "application/x-www-form-urlencoded",
                "method": "POST",
                "name": "Add RM Classification",
                "href": "/api/v2/nodes/2891606/rmclassifications",
                "body": "{\"displayPrompt\":false,\"enabled\":false,\"inheritfrom\":false,\"managed\":false}",
                "form_href": ""
            },
            "initiatedocumentworkflow": {
                "content_type": "",
                "method": "",
                "name": "",
                "href": "",
                "body": "initiate_in_smartview",
                "form_href": "",
                "wfList": [

                ]
            },
            "zipanddownload": {
                "content_type": "",
                "method": "POST",
                "name": "Zip and Download",
                "href": "/api/v2/zipanddownload",
                "body": "",
                "form_href": ""
            },
            "RemoveClassification": {
                "content_type": "application/x-www-form-urlencoded",
                "method": "POST",
                "name": "Remove Classificfation",
                "href": "/api/v2/nodes/2891606/rmclassifications",
                "body": "",
                "form_href": ""
            }
        },
        "map": {
            "default_action": "open"
        },
        "order": [
            "initiatedocumentworkflow",
            "Classify",
            "RemoveClassification",
            "zipanddownload"
        ]
    }
}

- req: the current HTTP request
- envelope: the REST API request's envelope

By changing the support variable "actions" you can make visible actions defined by scripts in CSVolume:CSSmartView:Commands

**/

actions[3156106].data["am_release"] = [
    body:"am_release"
]
actions[3156106].order.add("3156106")

CSSmartView:Commands

It's possible to define multiple commands in the same script and group them in the same sub-menu. E.g.

example 2

//Commands scripts can now return a list
return [
    [
        am:[
            exec:[
                mode:"group"// (1) This command will act as our flyout
            ]
        ]
        ,scope: "multiple"
        ,group: "info"
        ,flyout: "am_group" // (2) This command will act as our flyout
        ,baricon: null
        ,icon: null
        ,name: "Try Module Suite"
        ,command_key: "am_group"
        ,signature: "am_group"
    ],
    [
        am:[

            confirmation:[
                required:false,
                title:"",
                message:""
            ],
            panel:[
                width:40,
                cssClass:"",
                slides:[
                    [
                        title:"",
                        script:null
                    ]
                ]
            ],
            key:[
                code: 83
                ,message:""
                ,nogui:false
            ],
            exec:[
                mode:"script"
                ,script: 2644067
                ,params:[

                ]
                ,refresh_on_success:true
                ,on_success_action:""
                ,newtab:false
                ,url:""  
            ]
        ]
        ,baricon: null
        ,icon: null
        ,name: "Content Script"
        ,command_key: "am_content_script"
        ,signature: "am_content_script"
        ,scope: "multiple"
        ,flyout:"am_group"
        ,selfBlockOnly: false
    ]
    ...
]

Content Script scripts executed as commands can return execution information to the caller. E.g.

example 4

//Script code...
//Once done...notify caller
json([message:[type:'success', text:"Get the Module Suite. You won't need anything else.", details:"The Module Suite is a comprehensive framework of highly innovative solutions dedicated to OpenText™ Content Server, and includes all the tools you will need to extend, customize, and enrich your Content Server experience."]])