Alfresco share custom object-finder.js

Introduction

Share has really nice way of editing any type of node, editing is of course done using forms. Previously we talked about custom models and that object can be in a relationship with other objects. So how to connect two object using UI?

In this post we are going to extend component object-finder to allow user to search not only authorities but standard nodes like folder and custom also that we have created.

Model

For this example we have created a custom model that consists of one aspect ab:invoice  and one type  ab:buyer. Relevant part of model is listed below.

<types>
 <type name="ab:buyer">
  <title>Buyer</title>
  <parent>cm:folder</parent>
  <properties>
   <property name="ab:buyername">
     <type>d:text</type>
     <mandatory>true</mandatory>
   </property>
   <property name="ab:buyerid">
    <type>d:text</type>
    <mandatory>true</mandatory>
    </property>
  </properties>
 </type>
</types>


<aspects>
 <aspect name="ab:invoice">
  <title>Invoice</title>
  <properties>
   <property name="ab:invDate">
    <type>d:datetime</type>
   </property>
   <property name="ab:invNumber">
    <type>d:int</type>
   </property>
   <property name="ab:value">
    <type>d:double</type>
   </property>
  </properties>
  <associations>
   <association name="ab:buyerassoc">
    <title>Buyer Association</title>
    <source>
     <mandatory>true</mandatory>
     <many>true</many>
    </source>
    <target>
     <class>ab:buyer</class>
     <mandatory>true</mandatory>
     <many>false</many>
    </target>
   </association>
  </associations>
 </aspect>
</aspects>

Invoice aspect has several properties, invoice Date, invoice Number, invoice value and one association defining connection to instance of type ab:buyer , that will represent who is actually paying for a service and will receive an invoice. Buyer type is simple and consists of buyer name and buyer unique id.

Current situation

We have created one file Invoice.txt to which we have applied aspect ab:invoice, yeee , but we now want to set its properties. It is easy to see that property Invoice Date will be rendered as Date picker , that invoice number and value will be rendered as input fields so lets focus on association buyer . Image below is showing how this in rendered in the form for editing properties.

Edit Invoice Properties Form

Edit Invoice Properties Form

We will go into why this form is generated like this later, for now lets see what happens after we click on Select button. Image below shows this.

Select buyer standard

Select buyer standard

Dialog appears, we can see that initially we see the content of folder Buyers with its subfolders, that we can browse through the folders and after we find buyer we want clicking + will add him to the right.

Question

Question that imposes itself it what if we have 1000 buyers , how to we find one that we like, or if our buyers are not in one folder . This is a problem already solved in share if we want to set authority, but our buyer type is not authority and component object-finder does not want to allow us to search :'(.

Goal

Our goal is to allow developer to set form setting in such manner to allow user to select buyer not from infinite list but as a result of a search and to allow user to set query. Lets see time image of goal achieved below.

Buyer select with Query

Buyer select with Query

How does this new dialog works, it has search field and result of search are shown on the left. After user select desired buyer it appears on the right. We can choose what webscript serves the data and what results are, so in case we have scattered buyers across repository it is easy to see that this component will work in a great way.

Solution

Now we will show you how to achieve this result in few short files and configurations. First we must do is create custom object-finder.js component, in this case it will be called ab-object.finder.js. Custom component will override key parts of standard object-finder that will give us more freedom and possibilities.

(function() {
 var Dom = YAHOO.util.Dom, Event = YAHOO.util.Event;

   Alfresco.ABObjectFinder = function Alfresco_ABObjectFinder(htmlId, 
     currentValueHtmlId) {
   Alfresco.ABObjectFinder.superclass.constructor.call(this, htmlId,
     currentValueHtmlId);

   // Re-register with our own name
   this.name = "Alfresco.ABObjectFinder";
   Alfresco.util.ComponentManager.reregister(this);

   return this;
 };

   YAHOO.extend(Alfresco.ABObjectFinder, Alfresco.ObjectFinder, {
   _inAuthorityMode : function RTObjectSearcher__inAuthorityMode() {
   if(this.options.searchMode == "true"){
    return true;
   }else{
    return false;
  }
 },
});
})();

Few key parts here are create new component and call its super contrusctor, and extend Alfresco.ObjectFinder overriding its _inAuthorityMode method. _authorityMode function decides if it should show search form or browse form that can we have seen in previous images. We have previously explained how to create share modules so in the spirit of share modules you should put file ab-object-finder.js in path below written.

alfrescoblogAMPArchShare\src\main\amp\web\components\com\alfrescoblog\ab-object-finder

This is not enough by itself, we must tell share that this script has to be included when form renders. This is done adding next config in share-config-custom.xml

<config>
 <forms>
  <dependencies>
   <js src="/components/com/alfrescoblog/ab-object-finder/ab-object-finder.js" />
  </dependencies>
 </forms>
</config>

Another config needs to be set and that is form configuration, that will tell share to renders invoices properties in a desired way.

<config evaluator="aspect" condition="ab:invoice">
 <forms>
  <form>
   <field-visibility>
    <show id="ab:invDate" />
    <show id="ab:invNumber" />
    <show id="ab:value" />
    <show id="ab:buyerassoc" />
   </field-visibility>
   <appearance>
    <field id="sc:invDate" label-id="prop.ab_invDate" />
    <field id="sc:invNumber" label-id="prop.ab_invNumber" />
    <field id="sc:value" label-id="prop.ab_value" />
    <field id="ab:buyerassoc">
     <control
      template="/com/alfrescoblog/components/form/controls/association.ftl">
     <control-param name="showTargetLink">true</control-param>
     <control-param name="rootNode">/app:company_home/cm:Buyers/.
     </control-param>
     <control-param name="itemType">ab:buyer</control-param>
     <control-param name="searchMode">true</control-param>
     <control-param name="itemFamily">node</control-param>
     </control>
    </field>
   </appearance>
  </form>
 </forms>
</config>

Code previously shown tells us that if node has aspect ab:invoice next properties should be added to the form. Appearance part is more interesting and field ab:buyerassoc where we have specified template with number of control-parameters.

  • rootNode, that defines what is the initial folder that needs to be selected
  • searchMode, that defines if search form or standard form should be rendered
  • itemFamily, that is used when webscript is called that will return us the data we need

Association.ftl is custom made ftl file that will kickstart our custom component ab-object-finder.js ,so let see the code.

<#include "common/picker.inc.ftl" />

<#assign controlId = fieldHtmlId + "-cntrl">

<script type="text/javascript">//<![CDATA[
(function()
{
 <@renderPickerJS field "picker" />
 picker.setOptions(
 {
 <#if field.control.params.showTargetLink??>
 showLinkToTarget: ${field.control.params.showTargetLink},
 <#if page?? && page.url.templateArgs.site??>
 targetLinkTemplate: "${url.context}/page/site/${page.url.templateArgs.site!""}/document-details?nodeRef={nodeRef}",
 <#else>
 targetLinkTemplate: "${url.context}/page/document-details?nodeRef={nodeRef}",
 </#if>
 </#if>
 <#if field.control.params.allowNavigationToContentChildren??>
 allowNavigationToContentChildren: ${field.control.params.allowNavigationToContentChildren},
 </#if>
 itemType: "${field.endpointType}",
 multipleSelectMode: ${field.endpointMany?string},
 parentNodeRef: "alfresco://company/home",
 <#if field.control.params.rootNode??>
 rootNode: "${field.control.params.rootNode}",
 </#if>
 itemFamily: "${field.control.params.itemFamily!"node"}",
 displayMode: "${field.control.params.displayMode!"items"}",
 /* finderAPI: Alfresco.constants.PROXY_URI + "com/alfrescoblog/search/${field.control.params.itemFamily!"node"}",*/
 searchMode:"${field.control.params.searchMode!"false"}"
 });
})();
//]]></script>

<div class="form-field">
 <#if form.mode == "view">
 <div id="${controlId}" class="viewmode-field">
 <#if (field.endpointMandatory!false || field.mandatory!false) && field.value == "">
 <span class="incomplete-warning"><img src="${url.context}/res/components/form/images/warning-16.png" title="${msg("form.field.incomplete")}" /><span>
 </#if>
 <span class="viewmode-label">${field.label?html}:</span>
 <span id="${controlId}-currentValueDisplay" class="viewmode-value current-values"></span>
 </div>
 <#else>
 <label for="${controlId}">${field.label?html}:<#if field.endpointMandatory!false || field.mandatory!false><span class="mandatory-indicator">${msg("form.required.fields.marker")}</span></#if></label>
 
 <div id="${controlId}" class="object-finder">
 
 <div id="${controlId}-currentValueDisplay" class="current-values"></div>
 
 <#if field.disabled == false>
 <input type="hidden" id="${fieldHtmlId}" name="-" value="${field.value?html}" />
 <input type="hidden" id="${controlId}-added" name="${field.name}_added" />
 <input type="hidden" id="${controlId}-removed" name="${field.name}_removed" />
 <div id="${controlId}-itemGroupActions" class="show-picker"></div>
 
 <@renderPickerHTML controlId />
 </#if>
 </div>
 </#if>
</div>

Lets focus on two main lines of code

/* finderAPI: Alfresco.constants.PROXY_URI + "com/alfrescoblog/search/${field.control.params.itemFamily!"node"}",*/
 searchMode:"${field.control.params.searchMode!"false"}"
  • FinderAPI property is now commented out but if required it can be used if developer wants to create custom webscript to serve data.
  • searchMode property is set to false so standard form will be shown in that case.

We have included file picker.inc.ftl where we specify AbObjectFinder object.

<#assign compactMode = field.control.params.compactMode!false>

<#macro renderPickerJS field picker="picker">
 <@renderPickerJS field "picker" false/>
</#macro>

<#macro renderPickerJS field picker="picker" cloud=false>
 <#if field.control.params.selectedValueContextProperty??>
 <#if context.properties[field.control.params.selectedValueContextProperty]??>
 <#local renderPickerJSSelectedValue = context.properties[field.control.params.selectedValueContextProperty]>
 <#elseif args[field.control.params.selectedValueContextProperty]??>
 <#local renderPickerJSSelectedValue = args[field.control.params.selectedValueContextProperty]>
 <#elseif context.properties[field.control.params.selectedValueContextProperty]??>
 <#local renderPickerJSSelectedValue = context.properties[field.control.params.selectedValueContextProperty]>
 </#if>
 </#if>

 <#if cloud>
 var ${picker} = new Alfresco.CloudObjectFinder("${controlId}", "${fieldHtmlId}").setOptions(
 <#else>
 var ${picker} = new Alfresco.ABObjectFinder("${controlId}", "${fieldHtmlId}").setOptions(
 </#if>
 {
 <#if form.mode == "view" || (field.disabled && !(field.control.params.forceEditable?? && field.control.params.forceEditable == "true"))>disabled: true,</#if>
 field: "${field.name}",
 compactMode: ${compactMode?string},
 <#if field.mandatory??>
 mandatory: ${field.mandatory?string},
 <#elseif field.endpointMandatory??>
 mandatory: ${field.endpointMandatory?string},
 </#if>
 <#if field.control.params.startLocation??>
 startLocation: "${field.control.params.startLocation}",
 <#if form.mode == "edit" && args.itemId??>currentItem: "${args.itemId?js_string}",</#if>
 <#if form.mode == "create" && form.destination?? && form.destination?length &gt; 0>currentItem: "${form.destination?js_string}",</#if>
 </#if>
 <#if field.control.params.startLocationParams??>
 startLocationParams: "${field.control.params.startLocationParams?js_string}",
 </#if>
 currentValue: "${field.value}",
 <#if field.control.params.valueType??>valueType: "${field.control.params.valueType}",</#if>
 <#if renderPickerJSSelectedValue??>selectedValue: "${renderPickerJSSelectedValue}",</#if>
 <#if field.control.params.selectActionLabelId??>selectActionLabelId: "${field.control.params.selectActionLabelId}",</#if>
 selectActionLabel: "${field.control.params.selectActionLabel!msg("button.select")}",
 minSearchTermLength: ${field.control.params.minSearchTermLength!'1'},
 maxSearchResults: ${field.control.params.maxSearchResults!'1000'}
 }).setMessages(
 ${messages}
 );
</#macro>

<#macro renderPickerHTML controlId>
 <#assign pickerId = controlId + "-picker">
<div id="${pickerId}" class="picker yui-panel">
 <div id="${pickerId}-head" class="hd">${msg("form.control.object-picker.header")}</div>

 <div id="${pickerId}-body" class="bd">
 <div class="picker-header">
 <div id="${pickerId}-folderUpContainer" class="folder-up"><button id="${pickerId}-folderUp"></button></div>
 <div id="${pickerId}-navigatorContainer" class="navigator">
 <button id="${pickerId}-navigator"></button>
 <div id="${pickerId}-navigatorMenu" class="yuimenu">
 <div class="bd">
 <ul id="${pickerId}-navigatorItems" class="navigator-items-list">
 <li>&nbsp;</li>
 </ul>
 </div>
 </div>
 </div>
 <div id="${pickerId}-searchContainer" class="search">
 <input type="text" class="search-input" name="-" id="${pickerId}-searchText" value="" maxlength="256" />
 <span class="search-button"><button id="${pickerId}-searchButton">${msg("form.control.object-picker.search")}</button></span>
 </div>
 </div>
 <div class="yui-g">
 <div id="${pickerId}-left" class="yui-u first panel-left">
 <div id="${pickerId}-results" class="picker-items">
 <#nested>
 </div>
 </div>
 <div id="${pickerId}-right" class="yui-u panel-right">
 <div id="${pickerId}-selectedItems" class="picker-items"></div>
 </div>
 </div>
 <div class="bdft">
 <button id="${controlId}-ok" tabindex="0">${msg("button.ok")}</button>
 <button id="${controlId}-cancel" tabindex="0">${msg("button.cancel")}</button>
 </div>
 </div>

</div>
</#macro>

This will still not work, if we do not set API url we need to allow standard webscript that is being called to return queried data. For itemFamily property in form config we have specified node, but we could say buyer and then be able to customize how data is returned depending of type of data we require. We must override default script pickerchildren.get.js that is being called.

Overriding pickerchildren.get.js is done in alfresco module just putting it on path

alfrescoblogAMPArch\src\main\amp\config\alfresco\extension\templates\webscripts\org\alfresco\repository\forms\pickerchildren.get.js

File is changed just slightly so we are showing just key part

var childNodes=null;
 if(typeof argsSearchTerm!='undefined' && argsSearchTerm!=null && argsSearchTerm.length>0){
 childNodes = search.luceneSearch('PATH:"'+parent.qnamePath+'/*" AND @cm\\:name:*'+argsSearchTerm+'*');
 }else{
 childNodes = parent.childFileFolders(true, true, ignoreTypes, -1, maxResults, 0, "cm:name", true, null).getPage();
 }

What is the result of this code? If query is present then show the children of current folder which name contains value of the query. File can be downloaded here .

 

Summary

In this tutorial we have explained how to override object-finder.js component, how to control when you have search or browse form. You can use this tutorial when you need to override any other standard javascript component as well.

You could read more about working with custom types and alfresco and share modules .