Thursday, June 21, 2012

Type subcomponent evaluator ...

A new Alfresco 4.0.x feature is the customisation of Share by component replacement ... see http://blogs.alfresco.com/wp/ddraper/2011/07/29/sub-component-evaluations/

If you want to show/hide/change components based on types? Look no further. Here is an example that lets choose a type or all others not this type (inverted/negated).

The extension configuration holdes the reference to the Java-bean id and the parameters you define:

<evaluator type="share.evaluation.type" >
   <params>
      <nodetype>cm:folder</nodetype>
      <negate/>
   </params>
</evaluator>

Bean Id should be some thing like (replace <java-package>):

<bean id="share.evaluation.type"  class="<java-package>.TypeSubComponentEvaluator"/> 

 The Share java code needs to resolve the parameters, find the current nodeRef and perform a CMIS call to the repository to retrieve the type info.

public class TypeSubComponentEvaluator extends DefaultSubComponentEvaluator {

    @Override
    public boolean evaluate(RequestContext context, Map<String, String> arg1) {
        String requestedType = arg1.get("nodetype");
        boolean resultSuccess = arg1.get("negate") != null ? false : true;

        if (null == requestedType)
            return !resultSuccess;

        Map<String, String> uriTokens = context.getUriTokens();
        String nodeRef = uriTokens.get("nodeRef");
        if (nodeRef == null) {
            nodeRef = context.getParameter("nodeRef");
        }

        try {

            final Connector conn = context
                    .getServiceRegistry()
                    .getConnectorService()
                    .getConnector("alfresco", context.getUserId(),
                            ServletUtil.getSession());

            final Response response = conn.call("/api/node/"
                    + nodeRef.replace(":/", ""));
            if (response.getStatus().getCode() == Status.STATUS_OK) {

                String type = parseReponse(response);

                if (requestedType.equals(type))
                    return resultSuccess;
            } else {
                return !resultSuccess;
            }
        } catch (ConnectorServiceException cse) {
            cse.printStackTrace();
            return !resultSuccess;
        }

        return !resultSuccess;
    }

    private String parseReponse(Response response) {
        try {
            Document dom = DocumentBuilderFactory.newInstance()
                    .newDocumentBuilder().parse(response.getResponseStream());
            NodeList list = dom.getElementsByTagName("cmis:propertyId");
            int len = list.getLength();

            for (int i = 0; i < len; i++) {
                Element element = (Element) list.item(i);
                String propertyName = element
                        .getAttribute("propertyDefinitionId");
                String objectTypeId = null;
                if (propertyName.equals("cmis:objectTypeId")) {
                    objectTypeId = element.getElementsByTagName("cmis:value")
                            .item(0).getTextContent();
                    objectTypeId = objectTypeId.replaceAll("F:", "");
                }
                if (objectTypeId == null) {
                    continue;
                }
                return objectTypeId;
            }
        } catch (Exception exc) {
            exc.printStackTrace();
        }
        return null;
    }
}


I use this functionality to hide all comments for types not of a certain type :)

Tuesday, June 19, 2012

Custom admin console groups and sorting

In continuation of previous post, anybody trying this, will notice, that the sorting is out-of-control. For a quick Alfresco Share customisation, you could be tempted to hard-code some sorting into the freemaker templates. Well we could of cause also take the long road (some times better) and make a more general solution. So here goes. I propose a mechanism controlled by a group index configured in the properties along side the group label etc. The index can be configured for each locale/language thus alphabetical sorting is possible. The index sorting shall allow for multiple groups on same index and holes in the index sequence, and still handle the default (no group) tools.

This is implemented by customising the page-template controller ('console.js' in 'templates/org/alfresco/'), applying a property key for each group. The key is formatted like tools.group.<group>.index

Change group key generating loop in console.js:
            var group = "",
                groupLabelId = null,
                groupIndexId = null,
                paths = tool.scriptPath.split('/');
            if (paths.length > 1 && paths[paths.length - 2] == "console")
            {
               // found webscript package grouping
               group = paths[paths.length - 1];
               groupLabelId = "tool.group." + group;
               groupIndexId = "tool.group." + group + ".index";
            }
           
            var info =
            {
               id: scriptName,
               url: toolUrl,
               label: labelId,
               group: group,
               groupLabel: groupLabelId,
               groupIndex: groupIndexId,
               description: descId,
               selected: (currentToolId == scriptName)
            };


Then the component controller ('consol-tools.js' in 'site-webscripts/org/alfresco/components/console') will read the index (if present) and attempt sorting. The sorting rules are simple, precedence to groups with configured index, rest are places if index is 'free'. Group zero(0) is still for default group (no group name).

The component controller (console-tools.get.js) has large parts added:

/**
 * Admin Console Tools list component
 */

function main()
{
   // get the tool info from the request context - as supplied by the console template script
   var toolInfo = context.properties["console-tools"];
  
   // resolve the message labels
   for (var g = 0, group; g < toolInfo.length; g++)
   {
      group = toolInfo[g];
      for (var i = 0, info; i < group.length; i++)
      {
         info = group[i];
         info.label = msg.get(info.label);
         info.description = msg.get(info.description);
         if (info.group != "")
         {
            info.groupLabel = msg.get(info.groupLabel);
            info.groupIndex = msg.get(info.groupIndex);
         } else {
            info.groupIndex = 0;
         }
        
      }
   }

   var index = 0;
   var addedAtIndex = false;
   var indexArray = new Array();
   // Sort
   while (index < toolInfo.length) {
       for (var g = 0, group; g < toolInfo.length; g++)
       {
          group = toolInfo[g];
          for (var i = 0, info; i < group.length; i++)
          {
             info = group[i];
            
             if (info.groupIndex == index)
             {
                indexArray[indexArray.length] = index;
                 addedAtIndex=true;
                 break;
             }
          }
          if (addedAtIndex==true) break;
       }
      
       if (!addedAtIndex) {
           //find one with
           for (var g = 0, group; g < toolInfo.length; g++)
           {
              group = toolInfo[g];
              for (var i = 0, info; i < group.length; i++)
              {
                 info = group[i];
                
                 if (isNaN(info.groupIndex))
                 {
                    if (indexArray.length == index) indexArray[indexArray.length] = index;
                    info.groupIndex=index;
                     addedAtIndex=true;
                    
                 }
              }
              if (addedAtIndex==true) break;
           }
       }
      
       addedAtIndex = false;
       index++;
   }
  
   model.tools = toolInfo;
   model.indeces = indexArray;
}

main();


The sorting might still result in holes in the indexes range, so the freemarker template could be messy trying to figure out the ordering (group are not sorted in the transferred data (model.tools)). So to make it easy for the template, a new array is transferred with the actual used indexes. So the freemaker template can just go through the indexes finding the group with each index.

The component template (console-tools.get.html.ftl) is changed to:

<div id="${args.htmlid?html}-body" class="tool tools-link">
   <h2>${msg("header.tools")}</h2>
   <ul class="toolLink">
  
    <#list indeces as index>
          
        <#list tools as group>
               <#list group as tool>
                 <#if (tool.groupIndex)?number == index>
                     <#if tool_index=0 && tool.group != ""></ul><h3>${tool.groupLabel}</h3><ul class="toolLink"></#if>
                    <li class="<#if tool_index=0>first-link</#if><#if tool.selected> selected</#if>"><span><a href="${tool.id}" class="tool-link" title="${tool.description?html}">${tool.label?html}</a></span></li>
                 </#if>
             </#list>
        </#list>
           
    </#list>
   </ul>
</div>


Friday, June 15, 2012

Custom Admin Console groups

If you add alfresco share - admin console components, it is super easy: just provide a webscript-backed component, where the webscript family is set to 'admin-console'. This is the page-id.

See this excelent guide: http://blogs.alfresco.com/wp/wabson/2011/08/12/custom-admin-console-components-in-share/

Now what if you want it, grouped together with other custom components in a section like 'File Management' or 'Search' ... Well First you have to find the hard-code configuration-by-definition in the template controller 'console.js'. Paths are relative to site-webscripts.

It states in a comment, followed by code:


            // identify console tool grouping if any
            // simple convention is used to resolve group - last element of the webscript package path after 'console'
            // for example: org.alfresco.components.console.repository = repository
            //              org.yourcompany.console.mygroup = mygroup
            // package paths not matching the convention will be placed in the default root group
            // the I18N label should be named: tool.group.<yourgroupid>

            var group = "",
                groupLabelId = null,
                paths = tool.scriptPath.split('/');
            if (paths.length > 4 && paths[3] == "console")
            {
               // found webscript package grouping
               group = paths[4];
               groupLabelId = "tool.group." + group;
            }


This means component path 'com/mycomp/project/console/projectcomponents/component' will not be picked up, but 'com/mycomp/console/projectcomponents/component' will!

Side note: A more flexible approach could be to change it to:


            var group = "",
                groupLabelId = null,
                paths = tool.scriptPath.split('/');
            if (paths.length > 1 && paths[paths.length - 2] == "console")
            {
               // found webscript package grouping
               group = paths[paths.length - 1];
               groupLabelId = "tool.group." + group;
            }


This way both examples are valid and group is extracted as 'projectcomponents'.

Any way this gives you a new group after you configure you properties as well, for each group configure a property tool.group.<group>=<Your Group Title>

What you might notice now is a little to hard-coded assumption, that the first group is the default without a group is the first to be rendered in 'console-tools.get.html.ftl'. How ever the new group might be first, so the default group is placed wrong.


This can be fixed by customising the code to find the default group first:
<div id="${args.htmlid?html}-body" class="tool tools-link">
   <h2>${msg("header.tools")}</h2>
   <ul class="toolLink">
      <#list tools as group>
         <#list group as tool>
             <#if tool.group == "">
         <li class="<#if tool_index=0>first-link</#if><#if tool.selected> selected</#if>"><span><a href="${tool.id}" class="tool-link" title="${tool.description?html}">${tool.label?html}</a></span></li>
             </#if>
         </#list>
      </#list>
      <#list tools as group>
         <#list group as tool>
             <#if tool.group != "">
         <#if tool_index = 0></ul><h3>${tool.groupLabel}</h3><ul class="toolLink"></#if>
         <li class="<#if tool_index=0>first-link</#if><#if tool.selected> selected</#if>"><span><a href="${tool.id}" class="tool-link" title="${tool.description?html}">${tool.label?html}</a></span></li>
             </#if>
         </#list>
      </#list>
   </ul>
</div>

Tuesday, June 12, 2012

Default workflow message

Setting the default workflow message in share, based on the bpm package items

Two approaches possible. Override the java webscript FormUIGet or 'steal' the values from the start-workflow component (combobox).
see below form.control for a solution.

- Creating default messages ala: 'Review of myDoc.pdf'

I know the 'Review of ' is hardcoded, it could be pushed to a form.control.param :-)

Below is a freemaker template, which you can use in share-config-custom.xml ...




share-config-custom.xml 


<config evaluator="string-compare" condition="jbpm$wfl:customwfl">
        <forms>
            <form>
                <field-visibility>
                    <show id="bpm:workflowDescription" />
...
                </field-visibility>
                <appearance>
  
                    <field id="bpm:workflowDescription" label-id="workflow.field.message">
                        <control template="/com_redpill_linpro/components/form/controls/wf_preset_textarea.ftl">
               
                            <control-param name="style">width: 95%</control-param>
                        </control>
                    </field>
                    ...

wf_preset_textarea.ftl 


<#include "/org/alfresco/components/form/controls/textarea.ftl" />
 
 <#assign controlId = fieldHtmlId + "-cntrl">
    
 <#-- Below customization for default message -->

<script type="text/javascript">//<![CDATA[
            
   (function() {

            var onSuccess = function Wf_Preset_textArea_onSuccess(response)
             {
                var items = response.json.data.items,
                   item;
                if (0 < items.length) {
                    textArea = Dom.get("${fieldHtmlId}");
                    var message = "Review of ";
                    for (var i = 0, il = items.length; i < il; i++)
                    {
                      message += items[i].name;
                    }
                    textArea.innerHTML= message;
                }
    
             };
             
             var onFailure = function Wf_Preset_textArea_onFailure(response)
             {
                //empty
             };
      
             var startWorkflowComp = Alfresco.util.ComponentManager.get("${fieldHtmlId}".substring(0, "${fieldHtmlId}".indexOf("default") + 7));
             if (startWorkflowComp !== undefined) {
                Alfresco.util.Ajax.jsonRequest(
                {
                   url: Alfresco.constants.PROXY_URI + "api/forms/picker/items",
                   method: "POST",
                   dataObj:
                   {
                        items: startWorkflowComp.options.selectedItems.split(","),
                        itemValueType: "nodeRef"
                      
                   },
                   successCallback:
                   {
                      fn: onSuccess,
                      scope: this
                   },
                   failureCallback:
                   {
                      fn: onFailure,
                      scope: this
                   }
                });
            }
         })();
   
   //]]></script>

Monday, June 11, 2012

... little things matter

if you tried one of these error 40s (self-inflicted), this is hopefully a quick resolve for you :)

If you have a javascript webscript controller which uses JSON to communicate, remember to have 'json' in the webscript name, otherwise you do not get the 'magic' JSON object.

'testscript.post.js' (does not have a root object called 'json')
'testscript.post.json.js' (does)


You could also use the following start of the webscript (parsing the input data manually)
      // Check we have a json request
    if (typeof json === "undefined") {
        json = jsonUtils.toObject(requestbody.content);

        if (typeof json === "undefined") {

            if (logger.isWarnLoggingEnabled() == true) {
                logger.warn("Could not run webscript, because json object was undefined.");
            }

            status.setCode(501, "Could not run webscript, because json object was undefined.");
            return;
        }
    }