Difference between revisions of "Extending Admin UI"

m (s/extention/extension + format change for helloworld.xml + helloworld.js lis)
(Update with latest information about ZCS 5.0)
Line 1: Line 1:
The API for extending Admin UI is similar to [[Zimlets]]. Unlike [[Zimlets]], this API was not originally meant to be published. However people have asked about it, so I decided to document it. The intended audience is developers, who want to add custom modules to Zimra Admin UI, for example, people have asked about integrating Zimbra with samba. If you want to use this API, you need to have at least basic programming skills and be familiar with JavaScript.  
+
<center>Zimbra Administration Console 5.0 Framework Guide</center>
  
The easiest way to understand how to create extensions for Admin UI is by example. So, lets create a sample "Hello World" extension.
 
  
The example will add one tab to the Accounts view. The tab will be called "Hello World" and it will have some text.
+
= Introduction  =
 +
TBD
  
The extension will consist of two files:
+
= The Web Interface – Overview =
* helloworld.xml - XML definition of the extension
+
In general, the functionality of the administration console can be described as viewing, editing and creating data objects. Example of data objects are Account records, Domain records, Server records. The Web UI exchanges data objects with a server via HTTP connection. These data objects are described either in a format that can be parsed by JavaScript, such as XML Notation or JavaScript Object Notation (JSON).
* helloworld.js - JavaScript code of the extension
 
  
helloworld.xml:
+
The administration console follows Model-view-controller (MVC) design pattern. Data (M – model) is handled by Java beans inside a servlet container and stored in a database (LDAP and MySQL). Operations on the data (C – controller), such as read data from a database, write data to a database, send data to a client application, and receive data from a client application are handled by servlets that are running inside the servlet container. The presentation layer (V – view) of the admin console is an AJAX-based Web interface built on the same Zimbra AJAX Toolkit as the Zimbra mail UI.
  
----
+
The inner architecture of the Web interface also follows the MVC pattern, although not strictly. JavaScript classes in the Web interface can be grouped into three types: controllers, views and models. Models encapsulate data and provide methods for manipulating data. Views are responsible for defining and rendering the UI and Controllers glue views and models together.
  
    <zimlet name="helloworld" version="1.0" description="Sample Extension for Admin UI" extension="true">
+
The source tree of the admin web interface is organized in the following way. Top folders separate functionality areas, such as Account Management (“accounts” folder), Classes of Service Management (“cos” folder), Domain Management (“domains” folder), etc. Each top-level folder contains three sub folders: controller, model and view.
    <include>helloworld.js</include>
 
    </zimlet>
 
  
----
+
= UI Framework Guide =
 +
Admin UI framework is a HTML/JavaScript framework that consists of JavaScript Classes, constants and functions. For a programmer familiar with traditional OO programming techniques commonly used in Java and C++ languages, object oriented JavaScript may seem somewhat odd at first. There are three main types of classes in the Admin Console UI Framework: Models, Views and Controllers. The Xforms framework, and these classes are described in the following sections.
  
That's it. It is that short. It defines the name of the extension, description and included JS files. Note, that unlike Zimlet definition files it has a parameter extension="true". This is what tells zimlet utility that this is not a regular Zimlet, but an extension for Admin UI.
+
== Zimbra XForms Framework ==
So, moving forward.
+
Xforms UI framework facilitates rendering of UI elements, laying elements on the screen, feeding data from JavaScript objects (instances of model classes) to the UI elements and transferring user input to instances of models. Although, the name suggests that Xforms follows W3C Xforms specification, strictly speaking it does not do so. Therefore, even if you are familiar with other Xforms specification implementations – do not jump to any conclusions about Zimbra’s Xforms framework, it is a beast of its own kind (sorry Owen, it is). Designing a complicated user interface with Zimbra Xforms framework can be challenging, but the Xforms framework also makes things much easier once you know how to use it. Ultimately, a framework such as Xforms is essential when building a web-based user ???interface??? that has the main purpose of managing data objects on the serverSuch framework allows you to draw forms quickly and consistently. UI engineers at Zimbra either hate it or love it.
  
helloworld.js
+
== Models ==
 +
Models are classes that encapsulate data instances in the user interface. Some examples of the Model classes are:
  
----
+
accounts/model/ZaAccount.js
  
  function HelloWorldExtension() {}
+
cos/model/ZaCos.js
  
  HelloWorldExtension.AccountXFormModifier = function (xFormObject) {
+
domains/model/ZaDomain.js
    //find _TAB_BAR_ element
+
 
    var cnt = xFormObject.items.length;
+
All Model classes inherit from ZaItem base class. Each model class has a constructor function with one argument – a reference to the main application controller. The constructor should call ZaItem constructor function with two arguments – the reference to the main application controller and the name of the class. Following is an example of a model constructor:
    for(i = 0; i <cnt; i++) {
+
 
      if(xFormObject.items[i].type=="tab_bar")  
+
Code example
        break;
+
 
    }
+
  ZaItem.MY_TEST_CLASS =”MyTestClass”; // declare a string constant for referencing this class’ name
    //get a tab index
+
    var myTabIndex = ++ZaAccountXFormView.TAB_INDEX;
+
 
    //add a tab
+
/**
    xFormObject.items[i].choices.push({value:myTabIndex, label:"Hello World"});
+
    //define a form for my new tab. This ahs to be a _CASE_ element
+
<nowiki>*</nowiki>Create a constructor function with one argument
   
+
   var myTab = {type:_CASE_, relevant: ("instance[ZaModel.currentTab] == " + myTabIndex),
+
<nowiki>*</nowiki>@param app – reference to an instance of the main application controller.
                items: [{type:_OUTPUT_, label:null, value:"Hello World!"}]};
+
 
+
<nowiki>**/</nowiki>
    //find SWITCH element
+
    for(i = 0; i <cnt; i++) {
+
'''function''' MyTestClass(app) {
      if(xFormObject.items[i].type=="switch")  
+
        break;
+
  //required statements
    }
+
    //add my tab to the form
+
  if (arguments.length == 0) '''return'''<nowiki>; //precaution</nowiki>
    xFormObject.items[i].items.push(myTab);
+
 +
/* call ZaItem constructor function with one additional argument and pass “this” as the context of the function call */
 +
 +
  ZaItem.call('''this''', app,”MyTestClass”);
 +
 +
 
 +
  '''this'''.type = ZaItem.MY_TEST_CLASS; //set this object’s type
 +
 +
 
 +
  //optional statements
 +
 +
  '''this'''<nowiki>.attrs = []; //declare an container for this object’s attributes</nowiki>
 +
 +
  '''this'''._init(app); //call initialization methods
 +
 +
}
 +
 +
/* prototype-style declaration of inheritance */
 +
 +
MyTestClass.'''prototype''' = '''new '''ZaItem;
 +
 +
MyTestClass.'''prototype'''.constructor = MyTestClass;
 +
 
 +
 
 +
Each model class consists of constants and methods. Constants usually correspond to LDAP attributes of data objects on the server. These constants look like following:
 +
 
 +
ZaAccount.A_office = “physicalDeliveryOfficeName”;
 +
 +
ZaAccount.A_street = “street”;
 +
 +
ZaAccount.A_zip = “postalCode”;
 +
 +
ZaAccount.A_state = “st”;
 +
 +
ZaAccount.A_mailDeliveryAddress = “zimbraMailDeliveryAddress”;
 +
 +
 
 +
Methods of a model are divided into five areas:
 +
 
 +
* Loading
 +
* Parsing
 +
* Creation
 +
* Modification
 +
 
 +
* Removing
 +
 
 +
Each of these areas is implemented by a queue of methods. For example, an account may have three loading methods: method1 retrieves data from Zimbra Directory, method2 retrieves data from CRM database, and method3 retrieves data from posix account records. Each queue of methods is stored as a FIFO (First In – First Out) array of function references inside a static map. The following five static maps are available for storing method references:
 +
 
 +
*
 +
**
 +
*** ZaItem.loadMethods. Queues of methods that implement loading of a data instance from the server.
 +
*** ZaItem.initMethods/Queues of methods that implement initialization.
 +
*** ZaItem.createMethods. Queues of methods that implement creation of an instance on the server.
 +
*** ZaItem.modifyMethods. Queues of methods that implement modification of a data instance on the server.
 +
*** ZaItem.removeMethods. Queues of methods that implement removal of data instance from the server.
 +
 
 +
(sentence not complete)In each of these static maps a key is a name of the model class, e.g. “<tt>ZaAccount</tt>”, “<tt>ZaServer</tt>”, and a value is the array of function references, i.e. References to methods that implement loading of an Account instance from the server are stored in <tt><nowiki>ZaItem.loadMethods[“ZaAccount”]</nowiki></tt> array, and references to the methods that implement deletion of an Account instance from the server are stored in <tt><nowiki>ZaItem.removeMethods[“ZaAccount”]</nowiki></tt>.
 +
 
 +
=== ZaItem.initMethods ===
 +
Methods referenced in <tt>initMethods</tt> map are responsible for any post-constructor initialization. These methods are called from within <tt>ZaItem._init</tt> method which is called at the end of a constructor. <tt>ZaItem.initMethods</tt> is useful when extending an existing model class, because it allows adding statements that will be called at the end of the constructor call.
 +
 
 +
In the following example we add an initialization step to <tt>ZaServer</tt> object. The purpose of this additional step is to initialize two class members and assign default values. This example is the actual code from the com_zimbra_hsm admin extension which is available in the ZCS Network version. The purpose of the extension is to add tools for volume management to the Zimbra administration console.
 +
 
 +
Code Example
 +
 
 +
/**
 +
 +
<nowiki>* </nowiki>Declare the initialization method. Assume that ZaHSM constructor was
 +
 +
<nowiki>* defined earlier.</nowiki>
 +
 +
<nowiki>* </nowiki>@param app – reference to an instance of the main application controller.
 +
 +
<nowiki>**/</nowiki>
 +
 +
ZaHSM.serverInit = '''function''' (app) {
 +
 +
  '''this'''.hsm = '''new '''Object();
 +
 +
  '''this'''.hsm.pollInterval = 500;
 +
 +
}
 +
 +
/**
 +
 +
<nowiki>* Add the new reference to the initialization method to the array of</nowiki>
 +
 +
<nowiki>* references in ZaItem.initMethods map under the key “ZaServer”.</nowiki>
 +
 +
<nowiki>**/</nowiki>
 +
 +
'''if'''<nowiki>(ZaItem.initMethods[”ZaServer”]) {</nowiki>
 +
 +
  <nowiki>ZaItem.initMethods[”ZaServer”].push(ZaHSM.serverInit);</nowiki>
 +
 +
}
 +
 
 +
 
 +
=== ZaItem.loadMethods ===
 +
As mentioned earlier, methods referenced in <tt>loadMethods</tt> map are responsible for loading data from the server. If you want to find out how these methods are called, look at <tt>ZaItem.load</tt> method in the ZaItem.js file. (do you need to tell them where to find it?)
 +
 
 +
'''Code example'''
 +
 
 +
In the following example we create a function that loads some data from the server via a SOAP request. Note that this example assumes that there is a SOAP Handler for URN <tt>“mytestURN”</tt> that is aware of the SOAP command <tt>“MyTestRequest”</tt> and returs (do you mean returns) “<tt>MyTestResponse</tt>” document in JSON format; “<tt>MyTestRequest</tt>” SOAP command takes one argument “<tt>testArgument</tt>“.
 +
 
 +
//declare the method
 +
 +
MyTestClass.loadMethod = '''function'''(by, val) {
 +
 +
  '''if'''(!val)
 +
 +
    '''return'''<nowiki>;</nowiki>
 +
 +
 
 +
  '''var''' soapDoc = AjxSoapDoc.create(”MyTestRequest”, “urn:myTestURN”, '''null''');
 +
 +
  soapDoc.set(”testArgument”, “testValue”);
 +
 +
  '''var''' getAdditionalInfoCommand = '''new '''ZmCsfeCommand();
 +
 +
  '''var''' params = '''new '''Object();
 +
 +
  params.soapDoc = soapDoc;
 +
 +
  '''var''' resp =   <nowiki>getAdditionalInfoCommand.invoke(params).Body.MyTestResponse.myTestObj[0];</nowiki>
 +
 +
  '''this'''.initFromJS(resp);
 +
 +
}
 +
 +
 
 +
/**
 +
 +
<nowiki>* </nowiki> add the method reference to the queue of method references in 
 +
 +
<nowiki>* ZaItem.loadMethods map. “ZaAccount” key corresponds to methods that</nowiki>
 +
 +
<nowiki>* </nowiki> are called when loading a ZaAccount object
 +
 +
<nowiki>**/</nowiki>
 +
 +
'''if'''<nowiki>(ZaItem.loadMethods[”ZaAccount”]) {</nowiki>
 +
 +
  <nowiki>ZaItem.loadMethods[”ZaAccount”].push(MyTestClass.loadMethod);</nowiki>
 +
 +
}
 +
 
 +
=== ZaItem.createMethods ===
 +
Methods referenced in <tt>createMethods </tt>map are responsible for creating a data instance on the server. If you want to find out how these methods are called, look at <tt>ZaItem.create </tt>method in ZaItem.js file. In the ZCS administration console, every type of server object has a corresponding JavaScript class, e.g. Instances of Account objects stored in LDAP database on the server are represented as instanced of ZaAccount JavaScript class on the client side. Because multiple methods will be called in the UI when the UI creates an Account object on the server, ZaItem.create method creates an empty instance of ZaAccount JavaScript class before calling all the methods in <tt><nowiki>ZaItem.createMethods[”ZaAccount”]</nowiki></tt> queue. The same is true for other object types (ZaServer, ZaCos, ZaResource, ZaDomain, etc).
 +
 
 +
'''''Code example'''''
 +
 
 +
In the following example, we create a function that makes modifications to an Account object on the server after the object is created. This code example is the actual code from zimbra_samba extension that allows managing Samba accounts via Zimbra administration console.
 +
 
 +
 
 +
/**
 +
 +
<nowiki>*Declare the function assuming that ZaSamAccount constructor was defined </nowiki>
 +
 +
<nowiki>* earlier.</nowiki>
 +
 +
<nowiki>* @param tmpObj – a temporary javascript object that holds the data for the</nowiki>
 +
 +
<nowiki>* new instance of an Account object. TmpObj may or may not</nowiki>
 +
 +
<nowiki>* be an instance of ZaAccount JavaScript class.</nowiki>
 +
 +
<nowiki>* @param account – an instance of ZaAccount JavaScript class that was</nowiki>
 +
 +
<nowiki>* created in ZaItem.create method. At the time when</nowiki>
 +
 +
<nowiki>* ZaSamAccount.createMethod is called, account may already contain the data </nowiki>
 +
 +
<nowiki>* from the Account object that was created on the server</nowiki>
 +
 +
<nowiki>* during the previous calls.</nowiki>
 +
 +
<nowiki>* @param app – reference to an instance of the main application controller.</nowiki>
 +
 +
<nowiki>**/</nowiki>
 +
 +
ZaSamAccount.createMethod = '''function'''(tmpObj, account, app) {
 +
 +
   '''if'''<nowiki>(tmpObj.attrs[ZaAccount.A_password] &&</nowiki>
 +
 +
      <nowiki>tmpObj.attrs[ZaAccount.A_password].length > 0) {</nowiki>
 +
 +
        '''var''' soapDoc = AjxSoapDoc.create(
 +
 +
          ”ModifyAccountRequest”,
 +
 +
          “urn:zimbraAdmin”,
 +
 +
          '''null''');
 +
 +
 
 +
        soapDoc.set(”id”, account.id);
 +
 +
        '''var''' attr = soapDoc.set(”a”,  
 +
 +
        <nowiki>ZaSambaUtil.hex_md4(tmpObj.attrs[ZaAccount.A_password]));</nowiki>
 +
 +
        attr.setAttribute(”n”, ZaSamAccount.A_sambaNTPassword);
 +
 +
        '''var''' modifyAccCommand = '''new '''ZmCsfeCommand();
 +
 +
        '''var''' params = '''new '''Object();
 +
 +
        params.soapDoc = soapDoc;
 +
 +
        resp = modifyAccCommand.invoke(params).Body.ModifyAccountResponse;
 +
 +
        <nowiki>account.initFromJS(resp.account[0]);</nowiki>
 +
 +
        <nowiki>account[ZaAccount.A2_confirmPassword] = </nowiki>'''null'''<nowiki>;</nowiki>
 +
 +
  }
 +
 +
}
 +
 +
/**
 +
 +
<nowiki>* </nowiki>add the method reference to the queue of method references in 
 +
 +
<nowiki>* ZaItem.createMethods map. “ZaAccount” key corresponds to methods that</nowiki>
 +
 +
<nowiki>* </nowiki> are called when creatin an Account object on the server.
 +
 +
<nowiki>**/</nowiki>
 +
 +
'''if'''<nowiki>(ZaItem.createMethods[”ZaAccount”]) {</nowiki>
 +
 +
  <nowiki>ZaItem.createMethods[”ZaAccount”].push(ZaSamAccount.createMethod);</nowiki>
 +
 +
}
 +
 
 +
=== ZaItem.modifyMethods ===
 +
Methods referenced in <tt>modifyMethods</tt> map are responsible for modifying a data instance on the server. If you want to find out how these methods are called, look at <tt>ZaItem.modify</tt> method in ZaItem.js file.
 +
 
 +
=== ZaItem.removeMethods ===
 +
Methods referenced in <tt>removeMethods</tt> map are responsible for removing a data instance on the server. If you want to find out how these methods are called, look at <tt>ZaItem.remove</tt> method in ZaItem.js file.
 +
 
 +
=== myXModel ===
 +
MyXModel is a static member of each model class. This member is a JavaScript object. It contains meta-data that describes the structure of the data encapsulated in the model class in the Xforms terms. MyXModel is used only by the Xforms engine to determine how to bind a data instance to form elements.
 +
 
 +
=== Communicating with a server via SOAP/JSON ===
 +
As I mentioned earlier, model classes are responsible for communicating with a server. Although, there are cases when other types of objects send requests to a server and parse server responses. Communication with a server is done by sending SOAP requests and receiving responses in XML or JSON format. SOAP requests can be either synchronous or asynchronous. Zimbra AJAX Toolkit provides convenient APIs for sending and receiving SOAP/JSON requests via HTTP.
 +
 
 +
== Views ==
 +
Views are classes that define layout of objects on the screen and tie data with UI elements. In the admin console, there are two basic types of views: list views and Xform views. List views extend <tt>DwtListView</tt> class from Zimbra AJAX Toolkit and display a list of objects in a table with an optional tool bar. Xform views are based on Zimbra Xform framework and usually are used to display properties of a selected data object.
 +
 
 +
=== Navigation panel (aka Overview Panel) ===
 +
Overview panel contains the navigation tree. Nodes of the tree are created by the Overview Panel Controller.
 +
 
 +
=== List views ===
 +
The main purpose of a list view is to display lists of objects in a table-like layout. A list view consists of three elements: a list, a tool bar, and a pop-up menu. These elements are created by an instance of a controller class. MVC-addicts may argue that this is a wrong way to encapsulate functionality, and the right way would be to have a list view class be responsible for creating all of its elements and laying them out on the screen. (The following sentences are not clear, the sentence does not seem finished) While I agree that, as I mentioned earlier, we did not follow the MVC framework too closely on the client side and the role of controller classes is somewhat blurry. When you write extensions to the administration console you might as well avoid controllers.
 +
 
 +
Examples of list view classes in ZCS administration console are <tt>ZaAccountListView, ZaServerListView, ZaDomainListView,</tt> etc. All list view classes in the administration UI inherit from <tt>ZaListView</tt>, which in term inherits from <tt>DwtListView</tt>. If you are adding your own list view class, it is recommended that you follow this inheritance pattern, although that is not required as long as your class implements the required methods. However, it is required that any view class inherits from <tt>DwtComposite</tt> class. Here is one way to think about it for those of you who are more familiar with languages like C++ and Java. If JavaScript had abstract classes and pure abstract classes like C++ or interfaces like Java, there would have been a common interface (pure abstract class), e.g. <tt>IlistView</tt> that every list view class had to implement. <tt>DwtListView</tt> would have been a default implementation of that interface and would extend <tt>DwtComposite</tt> class. Since JavaScript does not enforce inheritance natively, any such enforcement would have been artificial and limiting. Therefore, the framework does not enforce the inheritance, but only advises it.
 +
 
 +
Methods that a list view class has to implement:
 +
 
 +
*
 +
**
 +
*** getTabIcon
 +
*** getTabTitle
 +
*** _sortColumn
 +
*** _setNoResultsHtml
 +
*** _createItemHtml
 +
 
 +
Following is an example of a simple list view class.
 +
 
 +
Code example
 +
 
 +
In the following example, we create a list view class that overwrites two list view methods <tt>toString</tt> and <tt>getTitle</tt>.
 +
 
 +
 
 +
/**
 +
 +
<nowiki>* @constructor</nowiki>
 +
 +
<nowiki>* @class ZaMyTestListView</nowiki>
 +
 +
<nowiki>* @param parent</nowiki>
 +
 +
<nowiki>**/</nowiki>
 +
 +
ZaMyTestListView= '''function'''(parent) {
 +
 +
  ZaListView.call('''this''', parent, '''null''', DwtControl.ABSOLUTE_STYLE);
 +
 +
}
 +
 +
 
 +
ZaMyTestListView.'''prototype''' = '''new '''ZaListView;
 +
 +
ZaMyTestListView.'''prototype'''.constructor = ZaMyTestListView;
 +
 +
 
 +
ZaMyTestListView.'''prototype'''.toString = '''function'''() {
 +
 +
  '''return''' “ZaMyTestListView”;
 +
 +
}
 +
 +
 
 +
ZaMyTestListView.'''prototype'''.getTitle = '''function''' () {
 +
 +
  '''return''' “My Test View”;
 +
 +
}
 +
 
 +
=== Form views ===
 +
Most form views display properties of a selected data object such as a server or an account. Therefore, form views consist of a tool bar, a tab bar and several HTML forms placed on tab cards. Similar to list views, form view elements are created by an instance of a controller class. All form views inherit from <tt>ZaTabView</tt>. If you are creating your own form views, you are not required to inherit your view classes from <tt>ZaTabView</tt>, but it is required that any view class inherits from <tt>DwtComposite</tt> class. In order to build forms quickly and consistently, we created a special framework for rendering and handling forms and called it Xforms in analogy to W3C Xforms proposal.
 +
 
 +
The following methods are common for all classes that implement form views: <tt>setObject</tt>, <tt>getTitle</tt> and <tt>myXFormModifier.setObject</tt> method takes an instance of a model class as its argument, caches the data of the instance in a member variable and passes the data to the Xforms form that is displayed on the view. In order to construct a form, we have to provide the Xforms framework with two meta-data objects, one that describes the data model for the form and another that describes the form elements and layout. The object that describes the data model is <tt>myXModel</tt> object described in section [#_toc155 #3.2.6.myXModel]. The object that describes form elements and layout is accessed by <tt>getMyXForm</tt> method of a view class. Default implementation of <tt>getMyXForm</tt> method is available in <tt>ZaTabView</tt> class, and it is not recommended to overwrite this method. The <tt>getMyXForm</tt> method follows the mechanism similar to the mechanism of initializing, loading, and modifying data instances as described in section [#_toc23 #3.2.Models]. There is a static map <tt>ZaTabView.XformModifiers</tt> where keys are names of the view classes and values are arrays of references to methods that define elements of a form, e.g. All methods that define form elements for a form that displays an account object are referenced in <tt><nowiki>ZaTabView.XformModifiers[“ZaAccountXFormView”]</nowiki></tt> array.
 +
 
 +
Following is an example of a simple form view class. This is an actual form view class for a view that manages Samba Domains in zimbra_samba admin extension.
 +
 
 +
Code example
 +
 
 +
 
 +
/**
 +
 +
<nowiki>* @class ZaSambaDomainXFormView</nowiki>
 +
 +
<nowiki>* @contructor</nowiki>
 +
 +
<nowiki>* @param parent</nowiki>
 +
 +
<nowiki>* @param app</nowiki>
 +
 +
<nowiki>**/</nowiki>
 +
 +
'''function''' ZaSambaDomainXFormView (parent, app) {
 +
 +
  /* call the parent constructor, “ZaSambaDomainXFormView” is the name of
 +
 +
  the CSS class for this view */
 +
 +
  ZaTabView.call('''this''', parent, app,”ZaSambaDomainXFormView”);
 +
 +
  /* initialize the Xform */
 +
 +
  '''this'''.initForm(ZaSambaDomain.myXModel,'''this'''.getMyXForm());
 +
 +
  /* assign a reference to the main application controller to the Xform
 +
 +
  instance, so that elements on the form can access the main application
 +
 +
  controller */
 +
 +
  '''this'''._localXForm.setController('''this'''._app);  
 +
 
  }
 
  }
  ZaTabView.XFormModifiers["ZaAccountXFormView"].push(HelloWorldExtension.AccountXFormModifier);
+
   
  
----
+
ZaSambaDomainXFormView.'''prototype''' = '''new '''ZaTabView();
Now you zip helloworld.xml and helloworld.js into helloworld.zip file and run zmzimletctl deploy helloworld.zip
+
Done!
+
ZaSambaDomainXFormView.'''prototype'''.constructor = ZaSambaDomainXFormView;
 +
 +
<nowiki>ZaTabView.XformModifiers[”ZaSambaDomainXFormView”] = </nowiki>'''new''' Array();
 +
  
Some more background: There are two types of views int Admin UI. List views and XForms views.  
+
/**
 +
 +
<nowiki>* @param entry – an instance of </nowiki> ZaSambaDomain class, that contains data
 +
 +
<nowiki>* for a Samba Domain object</nowiki>
 +
 +
<nowiki>**/</nowiki>
 +
 +
ZaSambaDomainXFormView.'''prototype'''.setObject = '''function''' (entry) {
 +
 +
  '''this'''._containedObject.attrs = '''new '''Object();
 +
  
- List views are ones that show you lists of Zimbra objects. When you click on Accounts node in the navigation tree, you open Accounts List View, when you click on Servers, you open Servers List View, etc
+
  /* create an object to store the local cache of the data */
 +
 +
  '''this'''._containedObject = '''new '''ZaSambaDomain('''this'''._app);
 +
  
- XForms views are views that show/edit/create a single Zimbra object. When you double click on an account in Accounts List view, you open an Account XForms view, when you click on Classes Of Service->default node, you open a COS XForms View.
+
  /**
 +
 +
  <nowiki>* </nowiki>BEGIN saving local copy of the data
 +
 +
  <nowiki>**/</nowiki>
 +
 +
  '''if'''(entry.id)
 +
 +
    '''this'''._containedObject.id = entry.id;
 +
  
Admin extensions allow you to modify anything on an XForms view, including toobar buttons. However, You cannot modify how the lists are shown in List views, in the List views you can only add new toolbar buttons and pop-up menues to List views.
+
  '''if'''(entry.name)
From the helloworld you can learn that in order to add anything to an XForms view you need to define your "modifier" method:
+
   
  HelloWorldExtension.AccountXFormModifier
+
    '''this'''._containedObject.name = entry.name;
Then you need to add this method to the map (ZaTabView.XFormModifiers):
+
  
  ZaTabView.XFormModifiers["ZaAccountXFormView"].push(HelloWorldExtension.AccountXFormModifier);
+
  '''for''' ('''var''' a in entry.attrs) {
 +
   
 +
    '''if'''<nowiki>(entry.attrs[a] </nowiki>'''instanceof''' Array) {
 +
 +
      '''this'''<nowiki>._containedObject.attrs[a] = </nowiki>'''new''' Array();
 +
 +
      '''var'''<nowiki> cnt = entry.attrs[a].length;</nowiki>
 +
 +
      '''for (var'''<nowiki> ix = 0; ix < cnt; ix++) {</nowiki>
 +
 +
        '''this'''<nowiki>._containedObject.attrs[a][ix]=entry.attrs[a][ix];</nowiki>
 +
 +
      }
 +
 +
    } '''else''' {
 +
 +
      '''this'''<nowiki>._containedObject.attrs[a] = entry.attrs[a];</nowiki>
 +
 +
    }
 +
 +
  }
 +
 +
  /**
 +
 +
  <nowiki>* </nowiki>END saving local copy of the data
 +
 +
  <nowiki>**/</nowiki>
 +
  
This statement adds your method to the list of methods that are called when "ZaAccountXFormView" is created.  
+
  /**
The map ZaTabView.XFormModifiers has the following arrays of methods:
+
 +
  <nowiki>* initialize the variable which is not part of </nowiki> ZaSambaDomain class, but
 +
 +
  <nowiki>* is used by the form</nowiki>
 +
 +
  <nowiki>**/</nowiki>
 +
 +
  '''if'''<nowiki>(!entry[ZaModel.currentTab])</nowiki>
 +
 +
    '''this'''<nowiki>._containedObject[ZaModel.currentTab] = “1”;</nowiki>
 +
 +
  '''else'''
 +
 +
    '''this'''<nowiki>._containedObject[ZaModel.currentTab] = entry[ZaModel.currentTab];</nowiki>
 +
  
  ZaTabView.XFormModifiers["ZaAccountXFormView"] - modifiers for Account view
+
  //pass the data to the form
ZaTabView.XFormModifiers["ZaCosXFormView"] - modifiers for Class of Service view
+
   
ZaTabView.XFormModifiers["ZaDLXFormView"] - modifiers for Distribution List view (this is not a list view, its an XForms view for a distributino list)
+
  '''this'''._localXForm.setInstance('''this'''._containedObject);
  ZaTabView.XFormModifiers["ZaDomainXFormView"] - modifiers for Domain view
+
   
ZaTabView.XFormModifiers["GlobalConfigXFormView"] - modifiers for Global Settings view
 
ZaTabView.XFormModifiers["ZaServerXFormView"] - modifiers for Server view
 
  
xFormObject argument which is passed to your modifier function (HelloWorldExtension.AccountXFormModifier in the example) will normally look like this:
+
  //trigger form redraw
 +
 +
  '''this'''.updateTab();
 +
 +
}
 +
  
----
+
ZaSambaDomainXFormView.'''prototype'''.getTitle = '''function''' () {
  /** you can take a look at files ZaDomainXFormView.js, ZaAccountXFormView.js, GlobalConfigXFormView.js etc, to see how each
+
  * xFormObject looks for each view. You will want to look at myXFormModifier methods in each of these classes
+
  '''return''' “Samba Domains”;
  **/
+
  {
+
}
  tableCssStyle:"width:100%;overflow:auto",
+
  items:[
+
 
     {type:_GROUP_, cssClass:"ZmSelectedHeaderBg", colSpan:"*", /* this defines the top element of the form, a header */
+
  /**
        items: [
+
            {type:_GROUP_, numCols:4, colSizes:["32px","350px","100px","250px"],
+
<nowiki>* Define a static method which defines form elements</nowiki>
                  items: [
+
                    {type:_AJX_IMAGE_, src:"Domain_32", label:null},
+
<nowiki>* @param xFormObject – a reference to the object that contains definitions </nowiki>
                    {type:_OUTPUT_, ref:ZaDomain.A_domainName, label:null,cssClass:"AdminTitle", rowSpan:2},
+
                    {type:_OUTPUT_, ref:ZaItem.A_zimbraId, label:ZaMsg.NAD_ZimbraID}
+
<nowiki>* of form elements. This object was created in </nowiki>
                  ]
+
         ]
+
<nowiki>* ZaTabView</nowiki>.'''prototype'''.getMyXForm prior to calling this method
 +
   
 +
  <nowiki>**/</nowiki>
 +
 +
  ZaSambaDomainXFormView.myXFormModifier = '''function'''(xFormObject) {
 +
 +
  xFormObject.tableCssStyle=”width:100%;overflow:auto;”;
 +
 +
 
 +
  <nowiki>xFormObject.items = [</nowiki>
 +
 +
     {type:_GROUP_,  
 +
 +
      cssClass:”ZmSelectedHeaderBg”,  
 +
 +
      colSpan: *,  
 +
 +
      id:”xform_header”,
 +
 +
      <nowiki>items: [</nowiki>
 +
 +
        {type:_GROUP_,
 +
 +
          numCols:4,
 +
 +
          <nowiki>colSizes:[”32px”,”350px”,”100px”,”250px”],</nowiki>
 +
 +
          <nowiki>items: [</nowiki>
 +
 +
            {type:_AJX_IMAGE_, src:”Domain_32”, label:'''null'''},
 +
 +
            {type:_OUTPUT_,  
 +
 +
              ref:ZaSambaDomain.A_sambaDomainName,  
 +
 +
              label:'''null''',cssClass:”AdminTitle”, rowSpan:2},
 +
 +
            {type:_OUTPUT_, ref:ZaSambaDomain.A_sambaSID, label:”sambaSID”}
 +
 +
          ]
 +
 +
         }
 +
 +
      ],
 +
 +
      cssStyle:”padding-top:5px; padding-left:2px; padding-bottom:5px”
 +
 
     },
 
     },
     {type:_TAB_BAR_,  ref:ZaModel.currentTab, /* this defines the tab bar */
+
          choices:[ /* this array defines the buttons in the tab bar */
+
     {type:_TAB_BAR_,  
              {value:1, label:ZaMsg.Domain_Tab_General}, /* each button in the tab bar is defined as an object that has "value" and "label" */
+
   
              {value:2, label:ZaMsg.Domain_Tab_GAL},
+
      ref:ZaModel.currentTab,
              {value:3, label:ZaMsg.Domain_Tab_Authentication}
+
        ],cssClass:"ZaTabBar"
+
      <nowiki>choices:[</nowiki>
 +
 +
        {value:1, label:ZaMsg.Domain_Tab_General}
 +
 +
      ],
 +
 +
      cssClass:”ZaTabBar”, id:”xform_tabbar”
 +
 
     },
 
     },
     {type:_SWITCH_, /* this object contains all the "tabs" */
+
         items:[
+
     {type:_SWITCH_,
           {type:_CASE_, relevant:"instance[ZaModel.currentTab] == 1", /* _CASE_ object defines a single tab */
+
               items:[ /* form elements for the first tab ar defined here */
+
      <nowiki>items:[</nowiki>
          },
+
          {type:_CASE_, relevant:"instance[ZaModel.currentTab] == 2",  
+
        {type:_ZATABCASE_,
              items:[ /* form elements for the second tab ar defined here */
+
          }
+
          <nowiki>relevant:”instance[ZaModel.currentTab] == 1”,</nowiki>
 +
 +
          <nowiki>colSizes:[”250px”,”*”],</nowiki>
 +
 +
          <nowiki>items:[</nowiki>
 +
 +
            {ref:ZaSambaDomain.A_sambaDomainName,
 +
 +
              type:_TEXTFIELD_,
 +
 +
              label:ZaMsg.Domain_DomainName,
 +
 +
                    onChange:ZaTabView.onFormFieldChanged},
 +
 +
            {ref:ZaSambaDomain.A_sambaSID,
 +
 +
              type:_TEXTFIELD_,
 +
 +
              label:”sambaSID”, width:300,
 +
 +
                    onChange:ZaTabView.onFormFieldChanged},
 +
 +
            {ref:ZaSambaDomain.A_sambaAlgorithmicRidBase,
 +
 +
              type:_TEXTFIELD_,
 +
 +
              label:”sambaAlgorithmicRidBase”,
 +
 +
              cssClass:”admin_xform_number_input”,
 +
 +
              onChange:ZaTabView.onFormFieldChanged}]
 +
 +
        }
 +
 +
]
 +
 +
          }
 +
 +
  ];
 +
 +
}
 +
 +
 
 +
/* put the reference to the method defined above into the static map with the key that corresponds to this view */
 +
 +
<nowiki>ZaTabView.XformModifiers[”ZaSambaDomainXFormView”].push(ZaSambaDomainXFormView.myXFormModifier); </nowiki>
 +
 
 +
==== ZaTabView.XFormModifiers ====
 +
Methods referenced in <tt>ZaTabView.XformModifiers</tt> map are responsible for defining meta-data that describes form elements and form layout in Xforms syntax. If you want to find out how these methods are called, look at <tt>ZaTabView.getMyXForm</tt> method in ZaTabView.js file.
 +
 
 +
==== ZaTabView.getMyXForm vs. ZaItem.myXModel ====
 +
A question you may have after reading the previous section is how does meta data returned by <tt>ZaTabView.getMyXForm</tt> interact with meta data in <tt>ZaItem.myXModel</tt>. Lets look at an example. Let’s say, we have an Account object to be rendered. <tt>ZaItem.myXModel</tt> will say that the account has a name, which is a string, and a quota which is an integer. The <tt>ZaItem.myXModel</tt> may also say that an account may have a Class of Service, which is an ID of a Class of Service object, and if the quota attribute of the account is NULL, than it defaults to the quota attribute in the account’s class of service. Metadata returned by <tt>ZaTabView.getMyXForm</tt> will say that the account’s name is to be rendered as an editable text box, and Class of Service is to be rendered as a drop down with three values to select from.
 +
 
 +
== Controllers ==
 +
If you have read the sections about Models and Views, then by now you are probably wondering how Views get the Data and how user actions translate into data changes. This is exactly what controllers’ role is. In the administration console, controllers are the glue between the views and the data. Controllers pass data from Models to Views and vice versa. There are four types of controllers in the administration console: list view controllers, form view controllers, main application controller, and overview panel controller. The last two controllers are singletons, and therefore there is only one instance of the main application controller and one instance of the overview panel controller per application. The base class for all controllers is ZaController. This class provides default implementations of several methods and holds maps of references to methods (similar to <tt>ZaTabView.XformModifiers, ZaItem.loadMethods</tt> and other maps of references discussed previously). The following maps of method references are important when extending existing views and creating new views:
 +
 
 +
* ZaController.initToolbarMethods
 +
* ZaController.initPopupMenuMethods
 +
 
 +
=== ZaController.initToolbarMethods ===
 +
Methods referenced in <tt>ZaController.initToolbarMethods</tt> map are responsible for adding buttons to toolbars. Similar to all other reference maps, keys in the map are names of controllers and values are arrays of references to methods. Methods referenced in this map are called by <tt>ZaController.'''prototype'''._initToolbar</tt>
 +
 
 +
Following is an example of using to add buttons to the toolbar. This example is taken from com_zimbra_cert_manager extension.
 +
 
 +
Code example
 +
 
 +
/**
 +
 +
<nowiki>* Method that adds button descriptors to this view’s toolbar</nowiki>
 +
 +
<nowiki>**/</nowiki>
 +
 +
ZaCertsServerListController.initToolbarMethod = '''function''' () {
 +
 +
  '''this'''._toolbarOperations.push(
 +
 +
    '''new '''ZaOperation(ZaOperation.VIEW,
 +
 +
    com_zimbra_cert_manager.TBB_view_cert,
 +
 +
    com_zimbra_cert_manager.TBB_view_cert_tt,
 +
 +
    “ViewCertificate”,
 +
 +
    “ViewCertificate”,
 +
 +
      '''new '''AjxListener(
 +
 +
        '''this''',
 +
 +
         ZaCertsServerListController.'''prototype'''.viewCertListener)
 +
 +
      )
 +
 +
  );
 +
 +
 
 +
  '''this'''._toolbarOperations.push(
 +
 +
    '''new '''ZaOperation(ZaOperation.NEW,
 +
 +
    com_zimbra_cert_manager.TBB_launch_cert_wizard,
 +
 +
    com_zimbra_cert_manager.TBB_launch_cert_wizard_tt,
 +
 +
    “InstallCertificate”,
 +
 +
    “InstallCertificate”,
 +
 +
    '''new '''AjxListener('''this''',
 +
 +
      ZaCertsServerListController.'''prototype'''._newCertListener)
 +
 +
    )
 +
 +
  );
 +
 +
 
 +
  '''this'''._toolbarOperations.push('''new '''ZaOperation(ZaOperation.NONE));
 +
 +
  '''this'''._toolbarOperations.push(
 +
 +
    '''new '''ZaOperation(ZaOperation.HELP,
 +
 +
      com_zimbra_cert_manager.TBB_Help,
 +
 +
      com_zimbra_cert_manager.TBB_Help_tt,
 +
 +
      “Help”,
 +
 +
      “Help”,
 +
 +
      '''new '''AjxListener('''this''', '''this'''._helpButtonListener)
 +
 +
    )
 +
 +
  );
 +
 +
}
 +
 +
 
 +
//add method reference to the map
 +
 +
<nowiki>ZaController.initToolbarMethods[”ZaCertsServerListController”].push(</nowiki>
 +
 +
  ZaCertsServerListController.initToolbarMethod);
 +
 
 +
=== ZaController.initPopupMenuMethods ===
 +
This map is very similar to the <tt>initToolbarMethods </tt>map. Methods referenced in this map define items that appear in the popup menu. Most of the times, items in the popup menu mirror toolbar buttons. Methods referenced in this map are called by <tt>ZaController.'''prototype'''._initPopupMenu</tt>.
 +
 
 +
=== _createUI method ===
 +
When you are adding a new view to the administration console, there are two methods that you have to implement: <tt>_createUI</tt>, and <tt>show. _createUI</tt> method is responsible for instantiating elements of the view and calling the view manager to lay out the elements on the screen.
 +
 
 +
Following is an example of <tt>_createUI</tt> method of a form view controller class in com_zimbra_cert_manager extension which implements certificate management. In this example, <tt>_createUI</tt> method creates a toolbar, and a form view.
 +
 
 +
Code example
 +
 
 +
ZaCertViewController.'''prototype'''._createUI = '''function''' () {
 +
 +
  '''try''' {
 +
 +
    //map of elements that constitute the view
 +
 +
    '''var''' elements = '''new '''Object();
 +
 +
    //instantiate the form view
 +
 +
    '''this'''._contentView = '''new '''ZaCertView( '''this'''._container, '''this'''._app );
 +
 +
    /* initialize toolbar buttons, this calls
 +
 +
    <nowiki>* ZaController</nowiki>.'''prototype'''._initToolbar method which will iterate
 +
 +
    <nowiki>* through all the methods referenced in</nowiki>
 +
 +
    <nowiki>* ZaController.initToolbarMethods[”ZaCertViewController”] array */</nowiki>
 +
 +
    '''this'''._initToolbar();
 +
 +
    '''if(this'''._toolbarOperations && '''this'''._toolbarOperations.length) {
 +
 +
      //instantiate the toolbar
 +
 +
      '''this'''._toolbar = '''new '''ZaToolBar(
 +
 +
        '''this'''._container,   
 +
 +
        '''this'''._toolbarOperations
 +
 +
      );
 +
 +
      //add the toolbar to the map
 +
 +
      <nowiki>elements[ZaAppViewMgr.C_TOOLBAR_TOP] = </nowiki>'''this'''._toolbar;
 +
 +
    }
 +
 +
 
 +
    //add the xform view to the map
 +
 +
    <nowiki>elements[ZaAppViewMgr.C_APP_CONTENT] = </nowiki>'''this'''._contentView;
 +
 +
    '''var''' tabParams = {
 +
 +
      openInNewTab: '''true''',
 +
 +
      tabId: '''this'''.getContentViewId(),
 +
 +
      tab: '''this'''.getMainTab()
 +
 +
    }
 +
 +
    //call view manager to layout all the elements on the screen   
 +
 +
    '''this'''._app.createView('''this'''.getContentViewId(), elements, tabParams);
 +
 +
    '''this'''._UICreated = '''true'''<nowiki>;</nowiki>
 +
 +
    '''this'''<nowiki>._app._controllers[</nowiki>'''this'''.getContentViewId ()] = this ;
 +
 +
  } '''catch''' (ex) {
 +
 +
      '''this'''._handleException(ex,
 +
 +
        “ZaCertViewController.'''prototype'''._createUI”,
 +
 +
        '''null''',
 +
 +
        '''false'''
 +
 +
      );
 +
 +
      '''return'''<nowiki>;</nowiki>
 +
 +
        }
 +
 +
}
 +
 
 +
Following is an example of <tt>_createUI</tt> method of a list view controller class in com_zimbra_cert_manager extension. In this example, <tt>_createUI </tt>method creates a toolbar, a popup menu, and a list view.
 +
 
 +
Code example
 +
 
 +
ZaCertsServerListController.'''prototype'''._createUI = '''function''' () {
 +
 +
  '''try''' {
 +
 +
    '''var''' elements = '''new '''Object();
 +
 +
    '''this'''._contentView = '''new '''ZaCertsServerListView('''this'''._container);
 +
 +
  /* this will call ZaController.'''prototype'''._initToolbar method, which will
 +
 +
  <nowiki>* iterate through methods referenced in </nowiki>
 +
 +
  <nowiki>* ZaController.initToolbarMethods[“ZaCertsServerListController”] array */</nowiki>
 +
 +
 
 +
    '''this'''._initToolbar();
 +
 +
    '''if(this'''._toolbarOperations && '''this'''._toolbarOperations.length) {
 +
 +
      '''this'''._toolbar = '''new '''ZaToolBar(
 +
 +
        '''this'''._container,
 +
 +
        '''this'''._toolbarOperations
 +
 +
      );
 +
 +
      <nowiki>elements[ZaAppViewMgr.C_TOOLBAR_TOP] = </nowiki>'''this'''._toolbar;
 +
 +
}
 +
 +
/* this will call ZaController.'''prototype'''._initPopupMenu method, which will * iterate through methods referenced in
 +
 +
<nowiki>* ZaController.initPopupMenuMethods[“ZaCertsServerListController”] array */</nowiki>
 +
 +
    '''this'''._initPopupMenu();
 +
 +
    '''if(this'''._popupOperations && '''this'''._popupOperations.length) {
 +
 +
      '''this'''._actionMenu =
 +
 +
        '''new '''ZaPopupMenu(
 +
 +
           '''this'''._contentView,
 +
 +
            “ActionMenu”,
 +
 +
            '''null''',
 +
 +
            '''this'''._popupOperations
 +
 +
        );
 +
 +
    }
 +
 +
    <nowiki>elements[ZaAppViewMgr.C_APP_CONTENT] = </nowiki>'''this'''._contentView;
 +
 +
    '''var''' tabParams = {
 +
 +
      openInNewTab: '''false''',
 +
 +
      tabId: '''this'''.getContentViewId(),
 +
 +
      tab: '''this'''.getMainTab()
 +
 +
    }
 +
 +
    //call view manager to layout all the elements on the screen
 +
 +
    '''this'''._app.createView('''this'''.getContentViewId(), elements, tabParams);
 +
 +
 
 +
    '''this'''._contentView.addSelectionListener(
 +
 +
      '''new '''AjxListener('''this''', '''this'''._listSelectionListener)
 +
 +
    );
 +
 +
    '''this'''._contentView.addActionListener(
 +
 +
      '''new '''AjxListener('''this''', '''this'''._listActionListener)
 +
 +
    );
 +
 +
    '''this'''._removeConfirmMessageDialog =
 +
 +
      '''new '''ZaMsgDialog(
 +
 +
        '''this'''._app.getAppCtxt().getShell(),
 +
 +
        '''null''',
 +
 +
        <nowiki>[DwtDialog.YES_BUTTON, DwtDialog.NO_BUTTON], </nowiki>
 +
 +
        '''this'''._app
 +
 +
      );
 +
 +
 
 +
    '''this'''._UICreated = '''true'''<nowiki>;</nowiki>
 +
 +
    '''this'''<nowiki>._app._controllers[</nowiki>'''this'''.getContentViewId ()] = this ;
 +
 +
  } '''catch''' (ex) {
 +
 +
      '''this'''._handleException(
 +
 +
        ex,
 +
 +
        “ZaCertsServerListController.'''prototype'''._createUI”,
 +
 +
        '''null''',
 +
 +
        '''false'''
 +
 +
      );
 +
 +
      '''return'''<nowiki>;</nowiki>
 +
 +
  }
 +
 +
    } 
 +
 
 +
=== show method ===
 +
This method is called to show a view. E.g.: when a user selects “Servers” node in the navigation tree, an instance of <tt>ZaOverviewPanelController</tt> will call <tt>ZaServerListController.prorotype.show</tt> method. This method’s responsibility is to call <tt>_createUI</tt> method, pass the data to view, and call the view manager to make the view visible. The method has two arguments. In case of list view controller, the first argument is the list of objects to be displayed in the list view, and the second argument is a Boolean flag that tells whether to create a new tab for the view.
 +
 
 +
Following is an example of <tt>show</tt> method of a list view controller class in com_zimbra_cert_manager extension.
 +
 
 +
Code example
 +
 +
/**
 +
 +
<nowiki>* </nowiki>@param list {ZaItemList} a list of ZaServer [mailto:%7B@link {@link] ZaServer} objects
 +
 +
<nowiki>* @param openInNewTab {Boolean} a flag that indicates whether to open the</nowiki>
 +
 +
<nowiki>* view in the new tab</nowiki>
 +
 +
<nowiki>**/</nowiki>
 +
 +
ZaCertsServerListController.'''prototype'''.show = '''function'''(list, openInNewTab) {
 +
 +
  '''if''' (!'''this'''._UICreated) {
 +
 +
    //create the UI components
 +
 +
    '''this'''._createUI();
 +
 +
  }
 +
 +
 
 +
  //pass the data to the view
 +
 +
  '''if''' (list != '''null''')
 +
 +
    '''this'''._contentView.set(list.getVector());
 +
 +
 
 +
  //tell the view manager to show the view
 +
 +
  '''this'''._app.pushView('''this'''.getContentViewId());
 +
 +
 
 +
  '''if''' (list != '''null''')
 +
 +
    '''this'''._list = list;
 +
 +
 
 +
  '''this'''.changeActionsState();
 +
 +
}
 +
 +
 
 +
In case of form view controller, the first argument is the object to be shown on the form, and the second argument is the id of the tab where the view will be displayed.
 +
 
 +
Following is an example of <tt>show</tt> method of a form view controller class in com_zimbra_cert_manager extension.
 +
 
 +
 
 +
Code example
 +
 
 +
 
 +
ZaCertViewController.'''prototype'''.show = '''function'''(certs, targetServerId) {
 +
 +
  if (!'''this'''._UICreated) {
 +
 +
    '''this'''._createUI();
 +
 +
  }
 +
 +
 
 +
  if (certs != '''null''') {
 +
 +
    '''this'''._contentView.set(certs, targetServerId);
 +
 +
  }
 +
 +
 
 +
  '''this'''._app.pushView('''this'''.getContentViewId());
 +
 +
}
 +
 
 +
=== Handling exceptions ===
 +
ZaController class has a helper method for handling and reporting exceptions: <tt>ZaController.'''prototype'''._handleException method</tt>. This method allows the caller to display an error message, restart the application and call a function after the application is restarted.The method takes five arguments:
 +
 
 +
# Exception object
 +
# Name of the function where the exception was caught (String value) or a reference to the function which should be executed after the application is restarted
 +
# Array of arguments for the function that will be executed when the application is restarted
 +
# Boolean flag that indicates whether the application needs to be restarted
 +
# The object to be used as the context of the function to be called after the application is restarted
 +
 
 +
Only the first argument is required, and the rest of the arguments are optional. It is generally not recommended to force the application to restart when an exception is caught. In fact, there is only one place in the code of Zimbra Administration Console that uses TRUE for the fourth argument to <tt>ZaController.'''prototype'''._handleException</tt> call.
 +
 
 +
When the fourth argument (boolean flag) is FALSE, _handleException method will show an error dialog with the details of the exception. For well-known exceptions, such as authentication failure, network error, the dialog will have a customized user-friendly message. If the server's stack trace is available for the exception it will be displayed in the error dialog.
 +
 
 +
=== List view controllers ===
 +
As the title suggests, list view controllers control list views. The responsibilities of a list view controller are:
 +
 
 +
*
 +
** instantiate and initialize the elements: toolbar, popup menu, list view
 +
** capture and dispatch user-generated events: button click in the toolbar, element selection in the list view, element selection in the popup menu, and double click in the list view
 +
** control the state of the buttons in the toolbar and elements in the popup menu. E.g.: determine whether a particular button should be enabled or disabled and whether a particular element in the popup menu should be enabled or disabled
 +
** handle some of the events generated by changes in data. E.g.: redraw the list view when an object is deleted from the view
 +
 
 +
The framework of the administration console provides a base class for list view controllers: <tt>ZaListViewController</tt> which inherits from <tt>ZaController</tt> class. <tt>ZaListViewController</tt> holds some additional method maps and provides default implementations for methods commonly used by list view controllers.
 +
 
 +
=== Form view controllers ===
 +
Form view controllers control XForm views. The responsibilities of a form view controller are:
 +
 
 +
* instantiate and initialize the elements: toolbar and form view
 +
* cache the data instance and pass it on to the form view
 +
* capture and dispatch button clicks in the toolbar
 +
* control the state of buttons in the toolbar. E.g.: enable “save” button if the user changes anything on the form
 +
* determine what data is modified and what data is unchanged, and call ZaItem.modify method to submit the changes to server
 +
 
 +
=== Main application controller ===
 +
The main application controller is implemented by <tt>ZaApp</tt> class. The only thing you really need to know about <tt>ZaApp</tt> is that it holds references to the instances of all other controllers and provides several utility methods.
 +
 
 +
=== Overview panel controller ===
 +
ZaOverviewPanelController instantiates ZaOverviewPanel class, creates nodes in the navigation panel and handles selection of the nodes.
 +
 
 +
== Message dialogs ==
 +
Zimbra administration console uses message dialogs to show messages and prompt the user with Yes/No questions.
 +
 
 +
== XForm Dialogs ==
 +
XForm dialogs are similar to message dalogs. The only difference is that an XForm dialog uses XForm’s framework to draw the contents of the dialog.
 +
 
 +
== Wizard Dialogs ==
 +
Wizard dialog is a form of XForm dialog. In addition to functionality provided by an XForm dialog, a Wizard dialog also provides methods for navigating a step-by-step wizard.
 +
 
 +
= Application Skins =
 +
TBD
 +
 
 +
= Extensions for Zimbra Administration Console =
 +
 
 +
== What is an extension for Zimbra administration console ==
 +
Thanks to the flexibility of JavaScript, it is possible to modify nearly every aspect of the administration console through the mechanism of extensions. The framework allows developers to add new views to the administration console, manage new data objects in the administration console, extend existing objects with new properties, and customize existing views. Here are some examples of what can be done through this mechanism:
 +
 
 +
* Integrate Zimbra with Samba and manage user and machine accounts through Zimbra administration console
 +
* Manage SSL certificates though Zimbra administration console
 +
* Expose Webmin, Plesk, cPanel and WHM Control Panels functionality through Zimbra administration console
 +
 
 +
In this chapter I will provide examples of extensions to the administration console.
 +
 
 +
== Understanding the components of an extension ==
 +
An extension for Zimbra administration console consists of a UI component and an optional server component.
 +
 
 +
The UI component consists of the following files:
 +
 
 +
* Extension manifest <br/> An XML file that describes the extension to the framework. The file should has the same name as the extension. I.e. for zimbra_samba extension, the XML file is zimbra_samba.xml.
 +
* Configuration template <br/> An XML file that describes configuration properties and default values.
 +
* <nowiki>*.properties file with localized string constants. This file has the same nae as the extension. </nowiki>I.e. for com_zimbra_backuprestore extension's strings file is com_zimbra_backuprestore.properties.
 +
* JavaScript files
 +
 
 +
The Server component of the extension may follow the server extension framework, but this is not a requirement. The server component can be a server-side script, or a servlet hosted on the same or on a different server.
 +
 
 +
== Loading extensions ==
 +
Extensions are loaded by the administration console UI after user authentication. If you want to know exactly how the process works, take a look at <tt>ZaSettings.init </tt>method.<tt> </tt>
 +
 
 +
First, the framework looks for the resource file that contains localized string constants. The resource file is generated at runtime at /zimbraAdmin/res/''name of the extension''.js. I.e. if the extension is zimbra_samba, then the resource file will be /zimbraAdmin/res/zimbra_samba.js. We will discuss resource files later in this chapter.
 +
 
 +
Next, the framework loads all the JavaScript files that are included in the extension. JavaScript files are loaded by dynamically creating <nowiki><script> tags to the document. An important consequence of this method of loading extensions is that after each javaSript file is loaded, all statements in the file are executed by the browser. </nowiki>
 +
 
 +
 
 +
== Creating content of an extension ==
 +
In this guide, we will focus on the UI component and the most generic and common uses of the admin extension framework:
 +
 
 +
* adding new views
 +
* modifying existing views
 +
* adding data objects
 +
* extending existing data objects
 +
 
 +
=== Adding a list view ===
 +
As you can see from the description of the framework, in order to add a list view to the Zimbra administration console, you need to create a list view class, and a list view controller class. Once the classes are implemented, you need a way to tell the administration console when to show the new list view. The easiest way to do this is to add a node to the navigation tree (aka Overview Panel) and show the new list view when the node is selected.
 +
 
 +
==== Creating a list view class ====
 +
Lets consider the following task for this example. We want to retrieve a list of sambaDomain objects from an LDAP server and show the list in the administration console. List views are described previously in details in the section List Views. Here, I will give an example of a real list view class which is used in zimbra_samba extension.
 +
 
 +
Code example
 +
 
 +
'''function''' ZaSambaDomainListView(parent)  {
 +
 +
  '''var''' className = '''null'''<nowiki>;</nowiki>
 +
 +
  '''var''' posStyle = DwtControl.ABSOLUTE_STYLE;
 +
 +
  '''var''' headerList = '''this'''._getHeaderList();
 +
 +
  ZaListView.call('''this''', parent, className, posStyle, headerList);
 +
 +
  '''this'''._appCtxt = '''this'''.shell.getData(ZaAppCtxt.LABEL);
 +
 +
  '''this'''.setScrollStyle(DwtControl.SCROLL);
 +
 +
}
 +
 +
 
 +
ZaSambaDomainListView.'''prototype''' = '''new '''ZaListView;
 +
 +
ZaSambaDomainListView.'''prototype'''.constructor = ZaSambaDomainListView;
 +
 +
 
 +
ZaSambaDomainListView.'''prototype'''.toString = '''function'''() {
 +
 +
  '''return''' “ZaSambaDomainListView”;
 +
 +
}
 +
 +
 
 +
ZaSambaDomainListView.'''prototype'''.getTitle = '''function''' () {
 +
 +
  '''return''' “Manage Samba Domains”;
 +
 +
}
 +
 +
/**
 +
 +
<nowiki>* </nowiki>Renders a single item as a DIV element.
 +
 +
<nowiki>*/</nowiki>
 +
 +
ZaSambaDomainListView.'''prototype'''._createItemHtml =
 +
 +
'''function'''(object, now, isDndIcon) {
 +
 +
  '''var''' html = '''new '''Array(50);
 +
 +
  '''var''' div = document.createElement(”div”);
 +
 +
  <nowiki>div[DwtListView._STYLE_CLASS] = “Row”;</nowiki>
 +
 +
  <nowiki>div[DwtListView._SELECTED_STYLE_CLASS] = </nowiki>
 +
 +
    <nowiki>div[DwtListView._STYLE_CLASS] + “-” + DwtCssStyle.SELECTED;</nowiki>
 +
 +
 
 +
  <nowiki>div.className = div[DwtListView._STYLE_CLASS];</nowiki>
 +
 +
  '''this'''.associateItemWithElement(object, div, DwtListView.TYPE_LIST_ITEM);
 +
 +
 
 +
  '''var''' idx = 0;
 +
 +
  <nowiki>html[idx++] = “<table width=’100%’ cellspacing=’2’ cellpadding=’0’>”;</nowiki>
 +
 +
  <nowiki>html[idx++] = “<tr>”;</nowiki>
 +
 +
  '''var''' cnt = '''this'''._headerList.length;
 +
 +
  '''for (var'''<nowiki> i = 0; i < cnt; i++) {</nowiki>
 +
 +
    '''var''' id = '''this'''<nowiki>._headerList[i]._id;</nowiki>
 +
 +
    '''if'''(id.indexOf(ZaSambaDomain.A_sambaSID)==0) {
 +
 +
      // name
 +
 +
      <nowiki>html[idx++] = </nowiki>
 +
 +
        <nowiki>“<td align=’left’ width=” + </nowiki>'''this'''<nowiki>._headerList[i]._width + “><nobr>”;</nowiki>
 +
 +
 
 +
      <nowiki>html[idx++] = </nowiki>
 +
 +
        <nowiki>AjxStringUtil.htmlEncode(object.attrs[ZaSambaDomain.A_sambaSID]);</nowiki>
 +
 +
      <nowiki>html[idx++] = “</nobr></td>”;</nowiki>
 +
 +
    } '''else''' '''if '''(id.indexOf(ZaSambaDomain.A_sambaDomainName)==0) {
 +
 +
      // description
 +
 +
      <nowiki>html[idx++] = </nowiki>
 +
 +
        <nowiki>“<td align=’left’ width=” + </nowiki>'''this'''<nowiki>._headerList[i]._width + “><nobr>”;</nowiki>
 +
 +
      <nowiki>html[idx++] = </nowiki>
 +
 +
          AjxStringUtil.htmlEncode(
 +
 +
          <nowiki>object.attrs[ZaSambaDomain.A_sambaDomainName]);</nowiki>
 +
 +
 
 +
      <nowiki>html[idx++] = “</nobr></td>”;</nowiki>
 +
 +
    }
 +
 +
    <nowiki>html[idx++] = “</tr></table>”;</nowiki>
 +
 +
    div.innerHTML = html.join(””);
 +
 +
    '''return''' div;
 +
 +
  }
 +
 +
    }   
 +
 +
 
 +
    ZaSambaDomainListView.'''prototype'''._getHeaderList = '''function'''() {
 +
 +
      '''var''' headerList = '''new''' Array();
 +
 +
      '''var''' sortable=1;
 +
 +
      <nowiki>headerList[0] = </nowiki>
 +
 +
        '''new '''ZaListHeaderItem(
 +
 +
          ZaSambaDomain.A_sambaDomainName,
 +
 +
          “Domain Name”,
 +
 +
          '''null''',
 +
 +
          200,
 +
 +
          '''null''',
 +
 +
          ZaSambaDomain.A_sambaDomainName,
 +
 +
          '''true''',
 +
 +
          '''true''');
 +
 +
 
 +
      <nowiki>headerList[1] = </nowiki>
 +
 +
    '''new '''ZaListHeaderItem(
 +
 +
      ZaSambaDomain.A_sambaSID,
 +
 +
      “sambaSID”,
 +
 +
      '''null''',
 +
 +
      '''null''',
 +
 +
      '''null''',
 +
 +
      ZaSambaDomain.A_sambaSID,
 +
 +
      '''true''',
 +
 +
      '''true''');
 +
 +
 
 +
      '''return''' headerList;
 +
 +
    }   
 +
 
 +
==== Constructing the view object ====
 +
In most cases, the constructor of any view class will look like the constructor in this example. All the constructor does is initializing default values and calling the parent constructor. When creating a list view class, you can always use the same code for the constructor.
 +
 
 +
Next two lines after the constructor defines the inheritance of the class
 +
 
 +
ZaSambaDomainListView.'''prototype''' = '''new '''ZaListView; ZaSambaDomainListView.'''prototype'''.constructor = ZaSambaDomainListView;
 +
 +
 
 +
All list view classes must have ZaListView as their prototype. However, javascript does not have a built-in mechanism to restrict inheritance; and therefore, nothing prevents you from creating a list view class that does not have ZaListView class as a prototype. As long as the list view class correctly implements all the methods of ZaListView and DwtListView classes, it will work.
 +
 
 +
==== Setting the title of the list view ====
 +
The next method <tt>ZaSambaDomainListView.'''prototype'''.getTitle</tt> is called by the view manager when the user switches to this list view. String returned by this method will be displayed in the address area. In the screen shot below, getTitle method returned “Manage Accounts” string when I clicked on Accounts in the navigation panel.
 +
 
 +
 
 +
[[Image:]]
 +
 
 +
 
 +
The string that is returned by the getTitle method can be localized by using *.properties files.
 +
 
 +
==== Rendering list items ====
 +
The next method <tt>ZaSambaDomainListView.'''prototype'''._createItemHtml</tt> is called by the parent class DwtListView when it renders the list. This method has to return a DIV object, which has to be created by calling <tt>document.createElement(“div”)</tt>. <tt>DwtListView</tt> calls <tt>_createItemHtml</tt> method for each item that has to be displayed in the list, and places DIVs returned by <tt>_createItemHtml</tt> on the list view. In order to your list look similar to other lists in the administration console, we should use the same CSS classes that are used in this example:
 +
 
 +
<nowiki>div[DwtListView._STYLE_CLASS] = “Row”;</nowiki>
 +
 +
<nowiki>div[DwtListView._SELECTED_STYLE_CLASS] = div[DwtListView._STYLE_CLASS] + “-” + DwtCssStyle.SELECTED;</nowiki>
 +
 +
<nowiki>div.className = div[DwtListView._STYLE_CLASS];</nowiki>
 +
 +
 
 +
That will ensure that the list looks like other lists and that selected list elements are properly highlighted.
 +
 
 +
The next statement is important if you plan to implement actions for list elements, such as opening an object in a form view to show more details or edit, deleting an object, etc.
 +
 
 +
'''this'''.associateItemWithElement(object, div, DwtListView.TYPE_LIST_ITEM);
 +
 +
Next, in this example we iterate through  '''this'''._headerList to output each cell of the column in the row. '''this'''._headerList is defined in the next method 
 +
 +
ZaSambaDomainListView.'''prototype'''._getHeaderList.
 +
 
 +
 
 +
This method returns an array of <tt>ZaListHeaderItem</tt> objects which are used to create the list header and define columns in the list view. If your list view has only one column, you do not have to implement this method.
 +
 
 +
==== Creating a list view controller class ====
 +
I will continue with the same example, showing a list of sambaDomain objects. If you look at the actaual ZaSambaDomainListController class, it has more functionality then what I will describe here. For simplicity, lets narrow the scope and assume that the tool bar for this list view has only one button: “Edit”, which will open the Xform view with details of the selected sambaDomain.
 +
 
 +
Code example
 +
 
 +
'''function '''ZaSambaDomainListController(appCtxt, container, app) {
 +
 +
  ZaListViewController.call('''this''',
 +
 +
  appCtxt,
 +
 +
  container,
 +
 +
  app,
 +
 +
  ”ZaSambaDomainListController”);
 +
 +
}
 +
 +
 
 +
ZaSambaDomainListController.'''prototype''' = '''new '''ZaListViewController();
 +
 +
ZaSambaDomainListController.'''prototype'''.constructor = ZaSambaDomainListController;
 +
 +
<nowiki>ZaController.initToolbarMethods[”ZaSambaDomainListController”] = </nowiki>'''new''' Array();
 +
 +
 
 +
ZaSambaDomainListController.'''prototype'''.show = function(list) {
 +
 +
  '''if '''(!'''this'''._UICreated) {
 +
 +
    '''this'''._createUI();
 +
 +
  }
 +
 +
 
 +
  '''if''' (list != '''null''')
 +
 +
    '''this'''._contentView.set(list.getVector());
 +
 +
 
 +
  '''this'''._app.pushView('''this'''.getContentViewId());
 +
 +
 
 +
  '''if''' (list != '''null''')
 +
 +
    '''this'''._list = list;
 +
 +
}
 +
 +
 
 +
ZaSambaDomainListController.initToolbarMethod = '''function''' () {
 +
 +
  '''this'''._toolbarOperations.push(
 +
 +
    '''new '''ZaOperation(
 +
 +
      ZaOperation.EDIT,
 +
 +
      ZaMsg.TBB_Edit,
 +
 +
      ZaMsg.SERTBB_Edit_tt,
 +
 +
      “Properties”,
 +
 +
      “PropertiesDis”,
 +
 +
      '''new '''AjxListener('''this''',
 +
 +
        ZaSambaDomainListController.'''prototype'''._editButtonListener)
 +
 +
      )
 +
 +
    );
 +
 +
}
 +
 +
 
 +
<nowiki>ZaController.initToolbarMethods[”ZaSambaDomainListController”].push(</nowiki>
 +
 +
ZaSambaDomainListController.initToolbarMethod);
 +
 +
 
 +
ZaSambaDomainListController.'''prototype'''._createUI = '''function''' () {
 +
 +
  '''try''' {
 +
 +
    '''var''' elements = '''new '''Object();
 +
 +
    '''this'''._contentView = '''new '''ZaSambaDomainListView('''this'''._container);
 +
 +
    '''this'''._initToolbar();
 +
 +
    '''if(this'''._toolbarOperations && '''this'''._toolbarOperations.length) {
 +
 +
      '''this'''._toolbar = '''new '''ZaToolBar(
 +
 +
                        '''this'''._container,
 +
 +
                        '''this'''._toolbarOperations);
 +
 +
      <nowiki>elements[ZaAppViewMgr.C_TOOLBAR_TOP] = </nowiki>'''this'''._toolbar;
 +
 +
    }
 +
 +
 
 +
    <nowiki>elements[ZaAppViewMgr.C_APP_CONTENT] = </nowiki>'''this'''._contentView;
 +
 +
    '''var''' tabParams = {
 +
 +
      openInNewTab: '''false''',
 +
 +
      tabId: '''this'''.getContentViewId(),
 +
 +
      tab: '''this'''.getMainTab()
 +
 +
          }
 +
 +
    '''this'''._app.createView('''this'''.getContentViewId(), elements,tabParams);
 +
 +
    '''this'''._contentView.addSelectionListener(
 +
 +
      '''new '''AjxListener('''this''','''this'''._listSelectionListener)
 +
 +
    );
 +
 +
    '''this'''._contentView.addActionListener(
 +
 +
      '''new '''AjxListener('''this''', '''this'''._listActionListener)
 +
 +
    );
 +
 +
    '''this'''._UICreated = '''true'''<nowiki>;</nowiki>
 +
 +
  } '''catch''' (ex) {
 +
 +
    '''this'''._handleException(ex,
 +
 +
      “ZaSambaDomainListController.'''prototype'''._createUI”,
 +
 +
      '''null''',
 +
 +
      '''false'''
 +
 +
    );
 +
 +
    '''return'''<nowiki>;</nowiki>
 +
 +
  }
 +
 +
}
 +
 
 +
==== Constructing a list view controller object ====
 +
The constructor is very simple, it has only one statement, which is to tall the parent constructor. Following the constructor are two statements that implement inheritance in JavaScript. The following statement is necessary only if you want your view to have a tool bar.
 +
 
 +
==== Creating contents of a list view ====
 +
Method <tt>ZaSambaDomainListController.'''prototype'''._createUI </tt>instantiates elements of the UI: tool bar and list view. After being instantiated, these elements are assigned to the main application controller which takes care of the layout
 +
 
 +
'''this'''._app.createView('''this '''.getContentViewId(), elements,tabParams);
 +
 +
 
 +
Object elements is a map where keys correspond to a predefined position of the element on the screen: <tt>ZaAppViewMgr.C_APP_CONTENT </tt>for the main content area and <tt>ZaAppViewMgr.C_TOOLBAR_TOP </tt>for the tool bar area. When we call '''this'''.<tt>_app.createView </tt>method, layout manager arranges the elements on the screen according to the application skin.
 +
 
 +
==== Showing the list view ====
 +
Next method is ZaSambaDomainListController.'''prototype'''.show.<tt> </tt>As the name suggests, this method is responsible for showing the view. The only argument that this method takes is the data object. There are no restrictions to what the data object is. In this case, it is an instance of ZaList. This statement '''this'''._contentView.set(list.getVector()); passes the data to the instance of the list view class. The following statement '''this'''._app.pushView('''this'''.getContentViewId());<tt> </tt>calls the main application controller's method “pushView” to push the DIV that contains this view to the top of the stack of divs that contain views, which makes the view visible.
 +
 
 +
==== Defining a tool bar for a list view ====
 +
<nowiki>ZaController.initToolbarMethods[”ZaSambaDomainListController”] = </nowiki>'''new''' Array();
 +
 +
 
 +
This statement initializes the array of method references in ZaController.initToolbarMethods map with the key that corresponds to this controller class. The key should be the same value that we passed to the parent constructor call in the constructor.
 +
 
 +
A tool bar is created by the <tt>ZaController.'''prototype'''._initToolbar </tt>method based on the '''this'''.<tt>_toolbarOperations</tt> array. The array consists of <tt>ZaOperation </tt>objects. Each <tt>ZaOperation</tt> object describes one button in a tool bar.
 +
 
 +
Code example
 +
 
 +
'''this'''._toolbarOperations.push(
 +
 +
    '''new '''ZaOperation(
 +
 +
    ZaOperation.EDIT,
 +
 +
    ZaMsg.TBB_Edit,
 +
 +
    ZaMsg.SERTBB_Edit_tt,
 +
 +
    “Properties”,
 +
 +
    “PropertiesDis”,
 +
 +
    '''new '''AjxListener('''this''',
 +
 +
      ZaSambaDomainListController.'''prototype'''._editButtonListener)
 +
 +
    )
 +
 +
); 
 +
 
 +
<tt>ZaController.'''prototype'''._initToolbar </tt>calls methods referenced in <tt><nowiki>ZaController.initToolbarMethods["ZaSambaDomainListController"] </nowiki></tt>array. In this example we add a reference to <tt>ZaSambaDomainListController.initToolbarMethod </tt>method to this array.
 +
 
 +
=== Registering the list view controller ===
 +
Before we add a handle to the new view to the navigation panel, we need to register the new controller with the main application controller (ZaApp). This can be done by adding a getter method to ZaApp class.
 +
 
 +
Code example
 +
 
 +
ZaZimbraAdmin._SAMBA_DOMAIN_LIST = ZaZimbraAdmin.VIEW_INDEX++;
 +
 +
 
 +
      ZaApp.'''prototype'''.getSambaDomainListController = '''function'''() {
 +
 +
  '''if''' ('''this'''<nowiki>._controllers[ZaZimbraAdmin._SAMBA_DOMAIN_LIST] == </nowiki>'''null''') {
 +
 +
    '''this'''<nowiki>._controllers[ZaZimbraAdmin._SAMBA_DOMAIN_LIST] = </nowiki>
 +
 +
      '''new''' ZaSambaDomainListController(
 +
 +
        '''this'''._appCtxt,
 +
 +
        '''this'''._container,  
 +
 +
        '''this'''
 +
 +
          );
 +
 +
  }
 +
 +
  '''return''' '''this'''<nowiki>._controllers[ZaZimbraAdmin._SAMBA_DOMAIN_LIST];</nowiki>
 +
 +
}
 +
 +
 
 +
A global constant <tt>ZaZimbraAdmin.VIEW_INDEX</tt> is a counter that keeps track of how many view controller classes exist in the application.
 +
 
 +
In ZCS 5.0 all list view controllers are singletons, although this design pattern is not enforced by the framework and will be hard (if at all possible) to enforce in JavaScript. Therefore, instead of usual implementation of singleton pattern, where we would have a private constructor and a getInstance method, we let ZaApp.'''prototype'''.getSambaDomainListController method take care of maintaining only one instance of the controller class. All other objects should use this method to access the list view controller.
 +
 
 +
=== Adding A Node to The Navigation Panel ===
 +
ZaOverviewPanelController class has API hooks similar to ZaModel, ZaController, and ZaTabView classes. Adding a node to the navigation panel is done in two steps. First, implement a method which will takes the navigation tree object as an argument and adds the new node to the tree. Second, add a reference to this method to ZaOverviewPanelController.treeModifiers array. In this example, we want to add a single node which will trigger the new list view described earlier.
 +
 
 +
Code example
 +
 
 +
Zambra.ovTreeModifier = '''function''' (tree) { 
 +
 +
  '''if'''(ZaSettings.SYSTEM_CONFIG_ENABLED) {
 +
 +
    '''this'''._sambaTi = '''new '''DwtTreeItem('''this'''._configTi); 
 +
 +
    '''this'''._sambaTi.setText(”Samba Domains”);
 +
 +
    '''this'''._sambaTi.setImage(”ZimbraIcon”);
 +
 +
    '''this'''._sambaTi.setData(
 +
 +
      ZaOverviewPanelController._TID,
 +
 +
      ZaZimbraAdmin._SAMBA_DOMAIN_LIST
 +
 +
    );
 +
 +
 
 +
    '''if'''(ZaOverviewPanelController.overviewTreeListeners) {
 +
 +
      <nowiki>ZaOverviewPanelController.overviewTreeListeners[ZaZimbraAdmin._SAMBA_DOMAIN_LIST] = Zambra.sambaDomainListTreeListener;</nowiki>
 +
 +
          }
 +
 +
        }
 +
 +
}
 +
 +
 
 +
'''if'''(ZaOverviewPanelController.treeModifiers)
 +
 +
  ZaOverviewPanelController.treeModifiers.push(Zambra.ovTreeModifier);
 +
 +
 
 +
Zambra.sambaDomainListTreeListener = function (ev) { 
 +
 +
  '''if'''('''this'''._app.getCurrentController()) {
 +
 +
    '''this'''._app.getCurrentController().switchToNextView(
 +
 +
      '''this'''._app.getSambaDomainListController(),
 +
 +
      ZaSambaDomainListController.'''prototype'''.show,
 +
 +
      ZaSambaDomain.getAll('''this '''._app)
 +
 +
    );
 +
 +
  } '''else''' {
 +
 +
    '''this'''._app.getSambaDomainListController().show(
 +
 +
      ZaSambaDomain.getAll('''this '''._app)
 +
 +
    );
 +
 +
  }
 +
 +
}
 +
 
 +
=== Adding a new tab to an existing form view ===
 +
Adding a tab to an existing form view is the easiest task in creating an extension. All you needto do is to define the form elements for the new tab and add the tab to the tab bar. Following is a code snippet from zimbra_posixaccount extension that adds a tab to the Account view.
 +
 
 +
Code example
 +
 
 +
'''if'''<nowiki>(ZaTabView.XformModifiers[”ZaAccountXFormView”]) {</nowiki>
 +
 +
  zimbra_posixaccount.AccountXFormModifier = '''function''' (xFormObject) {
 +
 +
    /* find the SWITCH element which is the parent element for all tabs */
 +
 +
    '''var''' cnt = xFormObject.items.length;
 +
 +
    '''var''' i = 0;
 +
 +
    '''for ('''<nowiki>i = 0; i <cnt; i++) {</nowiki>
 +
 +
      '''if'''<nowiki>(xFormObject.items[i].type==”switch”)</nowiki>
 +
 +
        break; //index i now points to the SWITCH element
 +
 +
            }
 +
 +
      //find the index of the next tab
 +
 +
      '''var''' posixTabIx = ++'''this'''.TAB_INDEX;
 +
 +
      //tab bar is the element with index 1
 +
 +
      '''var'''<nowiki> tabBar = xFormObject.items[1];</nowiki>
 +
 +
      //add the new tab button to the tab bar
 +
 +
      tabBar.choices.push({value:posixTabIx, label:”Posix Account”});
 +
 +
      //define meta data for new form elements
 +
 +
      '''var''' posixAccountTab = {
 +
 +
        type:_ZATABCASE_, numCols:1,
 +
 +
        <nowiki>relevant:(”instance[ZaModel.currentTab] == “ + posixTabIx),</nowiki>
 +
 +
        <nowiki>items: [</nowiki>
 +
 +
                {type:_ZAGROUP_,
 +
 +
            <nowiki>items:[</nowiki>
 +
 +
              {ref:ZaPosixAccount.A_gidNumber,
 +
 +
                type:_OSELECT1_, editable:'''false''',
 +
 +
                choices:'''this'''._app.getPosixGroupIdListChoices('''true'''),
 +
 +
                msgName:”Posix group”,
 +
 +
                label:”Posix group”,
 +
 +
                labelLocation:_LEFT_,
 +
 +
                onChange:ZaTabView.onFormFieldChanged},
 +
 +
 
 +
              {ref:ZaPosixAccount.A_gidNumber,
 +
 +
                type:_TEXTFIELD_,
 +
 +
                msgName:ZaPosixAccount.A_gidNumber,
 +
 +
                label:ZaPosixAccount.A_gidNumber,
 +
 +
                labelLocation:_LEFT_,
 +
 +
                onChange:ZaTabView.onFormFieldChanged,           
 +
 +
                cssClass:”admin_xform_number_input”},
 +
 +
 
 +
              {ref:ZaPosixAccount.A_uidNumber,
 +
 +
                type:_TEXTFIELD_,
 +
 +
                msgName:ZaPosixAccount.A_uidNumber,
 +
 +
                label:ZaPosixAccount.A_uidNumber,
 +
 +
                labelLocation:_LEFT_,
 +
 +
                onChange:ZaTabView.onFormFieldChanged, 
 +
 +
                cssClass:”admin_xform_number_input”},
 +
 +
 
 +
              {ref:ZaPosixAccount.A_homeDirectory,
 +
 +
                type:_TEXTFIELD_,
 +
 +
                msgName:ZaPosixAccount.A_homeDirectory,
 +
 +
                label:ZaPosixAccount.A_homeDirectory,
 +
 +
                labelLocation:_LEFT_,
 +
 +
                onChange:ZaTabView.onFormFieldChanged,
 +
 +
                width:250},
 +
 +
 
 +
            {ref:ZaPosixAccount.A_loginShell,
 +
 +
              type:_TEXTFIELD_, msgName:ZaPosixAccount.A_loginShell,
 +
 +
              label:ZaPosixAccount.A_loginShell,
 +
 +
              labelLocation:_LEFT_,
 +
 +
              onChange:ZaTabView.onFormFieldChanged,
 +
 +
               width:250}
 +
 +
              ] 
 +
 +
            }
 +
 +
          ]
 +
 +
        };
 +
 +
 
 +
    //add the new tab to the list of tabs
 +
 +
    <nowiki>xFormObject.items[i].items.push(posixAccountTab);</nowiki>
 +
 +
      }
 +
 +
 
 +
  <nowiki>ZaTabView.XformModifiers[”ZaAccountXFormView”].push(</nowiki>
 +
 +
    zimbra_posixaccount.AccountXFormModifier
 +
 +
  );
 +
 +
}
 +
 +
 
 +
First, we define a modifier function
 +
 
 +
zimbra_posixaccount.AccountXFormModifier = function (xFormObject)
 +
 
 +
 
 +
which will add new form elements to the existing form. Then we add the reference to the new modifier function to the array of modifier references for the Account view
 +
 
 +
<nowiki>ZaTabView.XformModifiers[”ZaAccountXFormView”].push(zimbra_posixaccount.AccountXFormModifier);</nowiki>
 +
 +
 
 +
When the Account view is instantiated, all functions referenced in this array will be called.
 +
 
 +
The argument of the modifier function is an object that contains meta data for the XForm. The object is being passed to each modifier method.
 +
 
 +
=== Adding a Form View ===
 +
Adding a form view is similar to adding a list view. You need to create a form view controller class, and a form view class, and then connect the form view controller either to a navigation tree or to a list view controller.
 +
 
 +
==== Creating a form view class ====
 +
Following the same example, we will create a form view class for displaying and editing a sambaDomain object.
 +
 
 +
'''function''' ZaSambaDomainXFormView (parent, app) {
 +
 +
  ZaTabView.call('''this''', parent, app,"ZaSambaDomainXFormView");
 +
 +
  '''this.'''initForm(ZaSambaDomain.myXModel,'''this'''.getMyXForm());
 +
 +
  '''this.'''_localXForm.setController('''this'''._app);
 +
 +
}
 +
 +
 
 +
ZaSambaDomainXFormView'''.prototype''' = '''new '''ZaTabView();
 +
 +
ZaSambaDomainXFormView'''.prototype'''.constructor = ZaSambaDomainXFormView;
 +
 +
<nowiki>ZaTabView.XFormModifiers[</nowiki>"ZaSambaDomainXFormView"] = '''new''' Array();
 +
 +
 
 +
ZaSambaDomainXFormView'''.prototype'''.setObject =
 +
 +
'''function''' (entry) {
 +
 +
  '''this.'''_containedObject = '''new '''ZaSambaDomain('''this'''._app);
 +
 +
  '''this.'''_containedObject.attrs = '''new''' Object();
 +
 +
  '''if('''entry.id)
 +
 +
    '''this.'''_containedObject.id = entry.id;
 +
 +
  '''if('''entry.name)
 +
 +
    '''this.'''_containedObject.name = entry.name<nowiki>;</nowiki>
 +
 +
 
 +
  '''for (var '''a '''in''' entry.attrs) {
 +
 +
      '''if('''<nowiki>entry.attrs[a] </nowiki>'''instanceof''' Array) {
 +
 +
        '''this.'''<nowiki>_containedObject.attrs[a] = </nowiki>'''new '''Array();
 +
 +
        '''var '''<nowiki>cnt = entry.attrs[a].</nowiki>length<nowiki>;</nowiki>
 +
 +
        '''for (var'''<nowiki> ix = 0; ix < cnt; ix++) {</nowiki>
 +
 +
'''  this.'''<nowiki>_containedObject.attrs[a][ix]=entry.attrs[a][ix];</nowiki>
 +
 +
        }
 +
 +
      } '''else''' {
 +
 +
        '''this'''<nowiki>._containedObject.attrs[a] = entry.attrs[a];</nowiki>
 +
 +
      }
 +
 +
    }
 +
 +
 
 +
    '''if'''<nowiki>(!entry[ZaModel.currentTab])</nowiki>
 +
 +
      '''this'''<nowiki>._containedObject[ZaModel.currentTab] = </nowiki>"1"<nowiki>;</nowiki>
 +
 +
    '''else'''
 +
 +
      '''this'''<nowiki>._containedObject[ZaModel.currentTab] = entry[ZaModel.currentTab];</nowiki>
 +
 +
 
 +
    '''this'''._localXForm.setInstance('''this'''._containedObject);
 +
 +
    '''this'''.updateTab();
 +
 +
}
 +
 +
 
 +
ZaSambaDomainXFormView'''.prototype'''.getTitle =
 +
 +
'''function''' () {
 +
 +
  '''return''' "Samba Domains"<nowiki>;</nowiki>
 +
 +
}
 +
 +
 
 +
ZaSambaDomainXFormView.myXFormModifier = '''function'''(xFormObject) {
 +
 +
  xFormObject.tableCssStyle="width:100%;overflow:auto;"<nowiki>;</nowiki>
 +
 +
 
 +
  <nowiki>xFormObject.items = [</nowiki>
 +
 +
    {type:_GROUP_,
 +
 +
      cssClass:"ZmSelectedHeaderBg",
 +
 +
      colSpan: "*",
 +
 +
      id:"xform_header",
 +
 +
      <nowiki>items: [</nowiki>
 +
 +
{type:_GROUP_,
 +
 +
      numCols:4,
 +
 +
                <nowiki>colSizes:[</nowiki>"32px","350px","100px","250px"],
 +
 +
<nowiki> items: [</nowiki>
 +
 +
            {type:_AJX_IMAGE_,
 +
 +
              src:"Domain_32",
 +
 +
              '''label''':'''null'''},
 +
 +
                  {type:_OUTPUT_,
 +
 +
                  ref:ZaSambaDomain.A_sambaDomainName,
 +
 +
                  '''label''':'''null''',
 +
 +
                  cssClass:"AdminTitle",
 +
 +
                  rowSpan:2},        {type:_OUTPUT_,
 +
 +
                  ref:ZaSambaDomain.A_sambaSID,
 +
 +
                  '''label''':"sambaSID"}
 +
 +
  ]
 +
 +
}
 +
 +
],
 +
 +
      cssStyle:"padding-top:5px; padding-left:2px; padding-bottom:5px"},
 +
 +
    {type:_TAB_BAR_,
 +
 +
      ref:ZaModel.currentTab,
 +
 +
      <nowiki>choices:[</nowiki>{value:1, '''label''':ZaMsg.Domain_Tab_General}],
 +
 +
      cssClass:"ZaTabBar",
 +
 +
      id:"xform_tabbar"
 +
 +
    },
 +
 +
    {type:_SWITCH_,
 +
 +
      <nowiki>items:[</nowiki>
 +
 +
      {type:_ZATABCASE_,  
 +
 +
      relevant:<nowiki>"instance[ZaModel.currentTab] == 1"</nowiki>,  
 +
 +
      <nowiki>colSizes:[</nowiki>"250px","*"],
 +
 +
      <nowiki>items:[</nowiki>
 +
 +
        {ref: ZaSambaDomain.A_sambaDomainName,
 +
 +
          type:_TEXTFIELD_,
 +
 +
          '''label''':ZaMsg.Domain_DomainName,
 +
 +
          onChange:ZaTabView.onFormFieldChanged
 +
 +
        },
 +
 +
        {ref: ZaSambaDomain.A_sambaSID,
 +
 +
          type:_TEXTFIELD_,
 +
 +
          '''label''':"sambaSID",
 +
 +
          width:300,
 +
 +
          onChange:ZaTabView.onFormFieldChanged
 +
 +
        },
 +
 +
        {ref: ZaSambaDomain.A_sambaAlgorithmicRidBase,
 +
 +
          type:_TEXTFIELD_,
 +
 +
          '''label''':"sambaAlgorithmicRidBase",
 +
 +
          cssClass:"admin_xform_number_input",
 +
 +
          onChange:ZaTabView.onFormFieldChanged
 +
 +
        }
 +
 
         ]
 
         ]
 +
 +
}
 +
 +
    ]
 +
 +
    }
 +
 +
  ];
 +
 +
}
 +
 +
 +
<nowiki>ZaTabView.XFormModifiers[</nowiki>"ZaSambaDomainXFormView"].push(ZaSambaDomainXFormView.myXFormModifier);
 +
 +
==== Constructing a form view object ====
 +
The contructor in the previous example has three statements. A call to the parent contructor
 +
 +
ZaTabView.call('''this''', parent, app,"ZaSambaDomainXFormView");
 +
 +
in which we pass the name of the new class as to the fourth argument. A call to _initForm method.
 +
 +
'''this'''.initForm(ZaSambaDomain.myXModel,'''this'''.getMyXForm());
 +
 +
First argument in this statement is meta data that describes a ZaSambaDomain object, which will be displayed in this view; second argument is metadata that describes the elements of the form.
 +
 +
Third statement assigns a reference to the main application controller to the form instance.
 +
 +
'''this'''._localXForm.setController('''this'''._app);
 +
 +
The purpose of this call is to let form elements access to utility methods of ZaApp instance.
 +
 +
==== Passing data to the form ====
 +
setObject method is responsible for passing data to the form. This method is called by the controller. Following statement passes the data to the Xfrorm
 +
 +
'''this'''._localXForm.setInstance('''this'''._containedObject);
 +
 +
In this example, we create a local copy of the data (this._containedObject) in the view instance before passing it on to the form in order to be able to compare the data to the original state after it is modified by the user. Then we pass the local copy of the data to the XForm.
 +
 +
==== Defining form fields ====
 +
When we create a new form view, we define form fields similar to how we define them when modifying an existing form view. In order to define form fields, we need to create a modifier function
 +
 +
ZaSambaDomainXFormView.myXFormModifier = '''function'''(xFormObject)
 +
 +
and add a reference to this function to the appropriate array of function references.
 +
 +
<nowiki>ZaTabView.XFormModifiers[</nowiki>"ZaSambaDomainXFormView"].push(ZaSambaDomainXFormView.myXFormModifier);
 +
 +
Because this is a new view, the array of references is not initialized yet, so in order to avoid a null-reference error, we need to create the array before we push any references in it. A good practice is to define the array right after the constructor definitions at the top of the file
 +
 +
<nowiki>ZaTabView.XFormModifiers[</nowiki>"ZaSambaDomainXFormView"] = '''new''' Array();
 +
 +
 +
==== Creating a form view controller class ====
 +
It is recommended that a form view controllers class extends ZaXFormViewController class. This way, the new class gets access to all default implementations of the common methods of a form view controller.
 +
 +
'''function''' ZaSambaDomainController(appCtxt, container, app) {
 +
 +
  ZaXFormViewController.call(
 +
 +
    '''this,''' appCtxt, container,app,
 +
 +
    "ZaSambaDomainController");
 +
 +
  '''this'''._UICreated = '''false'''<nowiki>;</nowiki>
 +
 +
  '''this'''._toolbarOperations = '''new''' Array();
 +
 +
}
 +
 +
 +
ZaSambaDomainController'''.prototype''' = '''new '''ZaXFormViewController();
 +
 +
ZaSambaDomainController'''.prototype'''.constructor = ZaSambaDomainController;
 +
 +
 +
<nowiki>ZaController.initToolbarMethods[</nowiki>"ZaSambaDomainController"] = '''new''' Array();
 +
 +
<nowiki>ZaController.setViewMethods[</nowiki>"ZaSambaDomainController"] = '''new''' Array();
 +
 +
 +
/**
 +
 +
<nowiki>* @method initToolbarMethod</nowiki>
 +
 +
<nowiki>* This method creates ZaOperation objects </nowiki>
 +
 +
<nowiki>* All the ZaOperation objects are added to this._toolbarOperations array which is then used to </nowiki>
 +
 +
<nowiki>* create the toolbar for this view.</nowiki>
 +
 +
<nowiki>* Each ZaOperation object defines one toolbar button.</nowiki>
 +
 +
<nowiki>* Help button is always the last button in the toolbar</nowiki>
 +
 +
<nowiki>**/</nowiki>
 +
 +
ZaSambaDomainController.initToolbarMethod =
 +
 +
'''function '''() {
 +
 +
  '''this'''._toolbarOperations.push (
 +
 +
    '''new '''ZaOperation(
 +
 +
      ZaOperation.SAVE,
 +
 +
      ZaMsg.TBB_Save,
 +
 +
      ZaMsg.SERTBB_Save_tt,
 +
 +
      "Save",
 +
 +
      "SaveDis",
 +
 +
      '''new '''AjxListener('''this''', '''this'''.saveButtonListener)
 +
 +
    )
 +
 +
  );
 +
 +
 +
  '''this'''._toolbarOperations.push(
 +
 +
    '''new '''ZaOperation(
 +
 +
      ZaOperation.CLOSE,
 +
 +
      ZaMsg.TBB_Close,
 +
 +
      ZaMsg.SERTBB_Close_tt,
 +
 +
      "Close",
 +
 +
      "CloseDis",
 +
 +
      '''new '''AjxListener('''this''', '''this'''.closeButtonListener)
 +
 +
    )
 +
 +
  );   
 +
 +
 +
}
 +
 +
<nowiki>ZaController.initToolbarMethods[</nowiki>"ZaSambaDomainController"].push(
 +
 +
ZaSambaDomainController.initToolbarMethod);
 +
 +
 +
/**
 +
 +
<nowiki>*@method setViewMethod </nowiki>
 +
 +
<nowiki>*@param entry - isntance of ZaSambaDomain class</nowiki>
 +
 +
<nowiki>*/</nowiki>
 +
 +
ZaSambaDomainController.setViewMethod =
 +
 +
'''function'''(entry) {
 +
 +
  '''if'''(entry.name && entry.id)
 +
 +
    entry.load("name", entry.name);
 +
 +
 +
  '''if'''(!'''this '''._UICreated)
 +
 +
    '''this'''._createUI();
 +
 +
 +
    '''this'''._app.pushView(this.getContentViewId());
 +
 +
  '''this'''._contentView.setDirty('''false''');
 +
 +
/* setObject is delayed to be called after pushView in order to avoid jumping of the view */
 +
 +
  '''this'''._contentView.setObject(entry);
 +
 +
  '''this'''._currentObject = entry;
 +
 +
}
 +
 +
 +
<nowiki>ZaController.setViewMethods[</nowiki>"ZaSambaDomainController"].push(
 +
 +
ZaSambaDomainController.setViewMethod);
 +
 +
 +
/**
 +
 +
<nowiki>* @method _createUI</nowiki>
 +
 +
<nowiki>**/</nowiki>
 +
 +
ZaSambaDomainController.'''prototype'''._createUI =
 +
 +
'''function '''() {
 +
 +
  '''this'''._contentView = '''this'''._view =
 +
 +
    '''new '''ZaSambaDomainXFormView(this._container, this._app);
 +
 +
 +
  '''this'''._initToolbar();
 +
 +
 +
  '''this'''._toolbar = '''new '''ZaToolBar(
 +
 +
    '''this'''._container,
 +
 +
    '''this'''._toolbarOperations);
 +
 +
 +
  '''var''' elements = '''new '''Object();
 +
 +
  <nowiki>elements[ZaAppViewMgr.C_APP_CONTENT] = </nowiki>'''this'''._contentView;
 +
 +
  <nowiki>elements[ZaAppViewMgr.C_TOOLBAR_TOP] = </nowiki>'''this'''._toolbar;
 +
 +
  '''var''' tabParams = {
 +
 +
      openInNewTab: '''true''',
 +
 +
      tabId: '''this'''.getContentViewId()
 +
 +
  }; 
 +
 +
  '''this'''._app.createView('''this'''.getContentViewId(), elements, tabParams);
 +
 +
  '''this'''<nowiki>._app._controllers[</nowiki>'''this'''.getContentViewId ()] = '''this'''<nowiki>; </nowiki> 
 +
 +
  '''this'''._UICreated = '''true'''<nowiki>;</nowiki>
 +
 +
}
 +
 +
 +
ZaSambaDomainController.'''prototype'''._saveChanges =
 +
 +
'''function''' () {
 +
 +
 +
  '''var''' mods = '''new '''Object();
 +
 +
  '''for (var '''a '''in''' obj.attrs) {
 +
 +
    '''if'''(a == ZaItem.A_objectClass )
 +
 +
      continue;
 +
 +
 +
    '''if'''('''this'''<nowiki>._currentObject.attrs[a] != obj.attrs[a] ) {</nowiki>
 +
 +
<nowiki>mods[a] = obj.attrs[a];</nowiki>
 +
 
     }
 
     }
  ]
+
 +
  }
 +
 +
 
 +
  '''try''' {
 +
 +
    '''this'''._currentObject.modify(mods);
 +
 +
    '''this'''.fireChangeEvent(this._currentObject);
 +
 +
  } '''catch''' (ex) {
 +
 +
    '''var''' detailStr = "";
 +
 +
    '''for (var '''prop '''in''' ex) {
 +
 +
      '''if'''<nowiki>(ex[prop] </nowiki>'''instanceof''' Function)
 +
 +
continue;
 +
 +
 
 +
      <nowiki>detailStr = detailStr + prop + " - " + ex[prop] + "\n";</nowiki>
 +
 +
    }
 +
 +
    '''this'''._handleException(ex,
 +
 +
      "ZaSambaDomainController.'''prototype'''._saveChanges",
 +
 +
      '''null''',
 +
 +
      '''false''');
 +
 +
    '''return''' '''false'''<nowiki>; </nowiki>
 +
 +
  }
 +
 +
 
 +
  '''this'''._contentView.setDirty('''false''');
 +
 +
  '''return''' '''true'''<nowiki>;</nowiki>
 +
 
  }
 
  }
 +
  
----
+
==== Constructing a form view controller object ====
 +
The constructor is similar to list view controller constructor. Only the first statement in the constructor is required – the call to the parent contructor. The last argument of the parent constructor call is the String representation of the name of this class.
  
"helloworld" was very simple, however totally disfunctional :) In real life you will probably want your extension to take some input from users, communicate to server and display meaningful data. So, lets start with sending/receiving data from the server.  
+
ZaXFormViewController.call(
We want to be able to call some URL with some parameters every time a Zimbra user is created. We will continue with Helloworld extension:
+
 
helloworld.js
+
    '''this,''' appCtxt, container,app,
----
+
HelloWorldExtension.createSambaAccount = function (tmpObj, account, app) {
+
    "ZaSambaDomainController");
    /** this method is called after zimbra account is created. account argument is an instande of ZaAccount class
+
 
    * and has all the information about a zimbra account
+
==== Creating contents of the form view  ====
    **/
+
Method <tt>ZaSambaDomainController.'''prototype'''._createUI </tt>instantiates elements of the UI: tool bar and form view. After being instantiated, these elements are assigned to the main application controller which takes care of the layout
    var url = ["https://localhost/mysambaWebService.php?action=createUser&username=",account.name, "&zimbraId=",account.id,
+
 
            "&email=",account.attrs[ZaAccount.A_mailDeliveryAddress]].join("");
+
'''this'''._app.createView('''this'''.getContentViewId(), elements, tabParams);
    var result=AjxRpc.invoke(null, url, null, null, true);
+
 +
 
 +
Object elements is a map where keys correspond to a predefined position of the element on the screen: <tt>ZaAppViewMgr.C_APP_CONTENT </tt>for the main content area and <tt>ZaAppViewMgr.C_TOOLBAR_TOP </tt>for the tool bar area. When we call <tt>'''this'''._app.createView </tt>method, layout manager arranges the elements on the screen according to the application skin.
 +
 
 +
==== Registering a form view controller ====
 +
Just like we had to register the list view controller, we also need to register the form view controller. However, in ZCS 5.0 form view controllers are not singletons. An instance of a controller is created for each open form. When the form is closed, the isntance is destroyed.
 +
 
 +
In order to register the form view controller we need to add another getter method to ZaApp class.  
 +
 
 +
Code example
 +
 
 +
      ZaApp.'''prototype'''.getSambaDomainController = '''function'''() {
 +
 +
        '''var''' c = '''new''' ZaSambaDomainController(
 +
 +
          '''this'''._appCtxt,
 +
 +
          '''this'''._container,
 +
 +
          '''this'''
 +
 +
        );
 +
 +
 
 +
  '''var''' ctrl = '''this'''.getSambaDomainListController();
 +
 +
  c.addChangeListener('''new''' AjxListener(ctrl, ctrl.handleChange));
 +
 +
  c.addCreationListener('''new''' AjxListener(ctrl, ctrl.handleCreation));
 +
 +
  c.addRemovalListener('''new''' AjxListener(ctrl, ctrl.handleRemoval));  
 +
 +
        '''return''' c;
 +
 
  }
 
  }
ZaItem.createMethods["ZaAccount"].push(HelloWorldExtension.createSambaAccount);
 
  
----
+
==== Showing the form view ====
Similar to ZaTabView.XFormModifiers, there is a map ZaItem.createMethods that holds references to methods that are called when objects are created. Right now, this map has entries only for ZaAccount object, but I will add more entries for all other objects.
+
In order to show the form view, you need to call <tt>show</tt> method of the corresponding form view controller. In this example, we do not overwrite this method, because the default implementation is sufficient. Default implementation of <tt>show</tt><nowiki> method runs through all functions referenced in ZaController.setViewMethods[</nowiki>"ZaSambaDomainController"] array. Therefore, we define <tt>ZaSambaDomainController.setViewMethod</tt> function and add it to the array.
  ZaItem.createMethods["ZaAccount"]
+
 
Besides, createMethods, there are also methods that are called, when an object is modified. Also, currently there is only one entry in that map:
+
==== Defining a tool bar for a form view ====
  ZaItem.modifyMethods["ZaAccount"]
+
<nowiki>ZaController.initToolbarMethods[</nowiki>"ZaSambaDomainController"] = '''new''' Array();
Modify method is applied to the instance of the object being modified. The statement that calls functions in modifyMethods is ZaItem.modifyMethods[key][i].call(this, mods), where mods is a map of modified attributes. At this point, I recommend looking at ZaItem.js and ZaAccount.js for more details on this, untill there is a better documentation (which hopefully will happen soon).
+
 +
 
 +
This statement initializes the array of method references in ZaController.initToolbarMethods map with the key that corresponds to this controller class. The key should be the same value that we passed to the parent constructor call in the constructor.
 +
 
 +
A tool bar is created by the <tt>ZaController.'''prototype'''._initToolbar </tt>method based on <tt>'''this'''._toolbarOperations</tt> array. First, <tt>ZaController.'''prototype'''._initToolbar </tt><nowiki>calls functions referenced in ZaController.initToolbarMethods[</nowiki>"ZaSambaDomainController"]<tt> </tt>array; next, it creates tool bar buttons based on the <tt>ZaOperation </tt>objects found in <tt>'''this'''._toolbarOperations</tt> array. Each <tt>ZaOperation</tt> object describes one button in a tool bar. In this example, the tool bar will have two buttons: Save and Close.
 +
 
 +
Code example
 +
 
 +
'''this'''._toolbarOperations.push (
 +
 
 +
    '''new '''ZaOperation(
 +
 +
      ZaOperation.SAVE,
 +
 +
      ZaMsg.TBB_Save,
 +
   
 +
      ZaMsg.SERTBB_Save_tt,
 +
 +
      "Save",
 +
 +
      "SaveDis",  
 +
 +
      '''new '''AjxListener('''this''', '''this'''.saveButtonListener)
 +
 +
    )
 +
 +
  );
 +
 +
 
 +
  '''this'''._toolbarOperations.push(
 +
 +
    '''new '''ZaOperation(
 +
 +
      ZaOperation.CLOSE,
 +
 +
      ZaMsg.TBB_Close,  
 +
 +
      ZaMsg.SERTBB_Close_tt,
 +
   
 +
      "Close",
 +
   
 +
      "CloseDis",
 +
 +
      '''new '''AjxListener('''this''', '''this'''.closeButtonListener)
 +
 +
    )
 +
 +
  );
 +
 
 +
 
 +
=== Understanding resource files ===
 +
Resource file of an extension is a text file with the name “name of the extension.properties”. I.e. for zimbra_samba extension, the resource file is zimbra_samba.properties. The purpose of the resource file is to allow developers to localize extensions. A resource file consists of name value pairs, where a name is a name of a constant, and value is plain text or a pattern. As mentioned earlier, a resource file is translated into JavaScript file by Zimbra server when a browser requests the resource. For example, if your resource file is zimbra_samba.properties, and it contains the following name-value pair
 +
 
 +
      Domain_name_label = Samba domain name
 +
 
 +
 
 +
The generated JavaScript file will be zimbra_samba.js and will contain the following code:
 +
 
 +
'''function''' zimbra_samba () {}
 +
 +
zimbra_samba. Domain_name_label = “Samba domain name”;
 +
 
 +
 
 +
Thus, if you add resource files to your extension, instead of using strings for displayed text, you can use constants. In order to avoid null-reference errors, resource file is always loaded before all other JavaScript files of an extension.

Revision as of 00:43, 21 November 2007

Zimbra Administration Console 5.0 Framework Guide


Contents

Introduction

TBD

The Web Interface – Overview

In general, the functionality of the administration console can be described as viewing, editing and creating data objects. Example of data objects are Account records, Domain records, Server records. The Web UI exchanges data objects with a server via HTTP connection. These data objects are described either in a format that can be parsed by JavaScript, such as XML Notation or JavaScript Object Notation (JSON).

The administration console follows Model-view-controller (MVC) design pattern. Data (M – model) is handled by Java beans inside a servlet container and stored in a database (LDAP and MySQL). Operations on the data (C – controller), such as read data from a database, write data to a database, send data to a client application, and receive data from a client application are handled by servlets that are running inside the servlet container. The presentation layer (V – view) of the admin console is an AJAX-based Web interface built on the same Zimbra AJAX Toolkit as the Zimbra mail UI.

The inner architecture of the Web interface also follows the MVC pattern, although not strictly. JavaScript classes in the Web interface can be grouped into three types: controllers, views and models. Models encapsulate data and provide methods for manipulating data. Views are responsible for defining and rendering the UI and Controllers glue views and models together.

The source tree of the admin web interface is organized in the following way. Top folders separate functionality areas, such as Account Management (“accounts” folder), Classes of Service Management (“cos” folder), Domain Management (“domains” folder), etc. Each top-level folder contains three sub folders: controller, model and view.

UI Framework Guide

Admin UI framework is a HTML/JavaScript framework that consists of JavaScript Classes, constants and functions. For a programmer familiar with traditional OO programming techniques commonly used in Java and C++ languages, object oriented JavaScript may seem somewhat odd at first. There are three main types of classes in the Admin Console UI Framework: Models, Views and Controllers. The Xforms framework, and these classes are described in the following sections.

Zimbra XForms Framework

Xforms UI framework facilitates rendering of UI elements, laying elements on the screen, feeding data from JavaScript objects (instances of model classes) to the UI elements and transferring user input to instances of models. Although, the name suggests that Xforms follows W3C Xforms specification, strictly speaking it does not do so. Therefore, even if you are familiar with other Xforms specification implementations – do not jump to any conclusions about Zimbra’s Xforms framework, it is a beast of its own kind (sorry Owen, it is). Designing a complicated user interface with Zimbra Xforms framework can be challenging, but the Xforms framework also makes things much easier once you know how to use it. Ultimately, a framework such as Xforms is essential when building a web-based user ???interface??? that has the main purpose of managing data objects on the serverSuch framework allows you to draw forms quickly and consistently. UI engineers at Zimbra either hate it or love it.

Models

Models are classes that encapsulate data instances in the user interface. Some examples of the Model classes are:

accounts/model/ZaAccount.js

cos/model/ZaCos.js

domains/model/ZaDomain.js

All Model classes inherit from ZaItem base class. Each model class has a constructor function with one argument – a reference to the main application controller. The constructor should call ZaItem constructor function with two arguments – the reference to the main application controller and the name of the class. Following is an example of a model constructor:

Code example

ZaItem.MY_TEST_CLASS =”MyTestClass”; // declare a string constant for referencing this class’ name

/**

*Create a constructor function with one argument

*@param app – reference to an instance of the main application controller.

**/

function MyTestClass(app) {

  //required statements

  if (arguments.length == 0) return; //precaution

/* call ZaItem constructor function with one additional argument and pass “this” as the context of the function call */

  ZaItem.call(this, app,”MyTestClass”); 

  this.type = ZaItem.MY_TEST_CLASS; //set this object’s type

  //optional statements

  this.attrs = []; //declare an container for this object’s attributes

  this._init(app); //call initialization methods

}

/* prototype-style declaration of inheritance */

MyTestClass.prototype = new ZaItem; 

MyTestClass.prototype.constructor = MyTestClass; 


Each model class consists of constants and methods. Constants usually correspond to LDAP attributes of data objects on the server. These constants look like following:

ZaAccount.A_office = “physicalDeliveryOfficeName”;

ZaAccount.A_street = “street”;

ZaAccount.A_zip = “postalCode”;

ZaAccount.A_state = “st”;

ZaAccount.A_mailDeliveryAddress = “zimbraMailDeliveryAddress”;

Methods of a model are divided into five areas:

  • Loading
  • Parsing
  • Creation
  • Modification
  • Removing

Each of these areas is implemented by a queue of methods. For example, an account may have three loading methods: method1 retrieves data from Zimbra Directory, method2 retrieves data from CRM database, and method3 retrieves data from posix account records. Each queue of methods is stored as a FIFO (First In – First Out) array of function references inside a static map. The following five static maps are available for storing method references:

      • ZaItem.loadMethods. Queues of methods that implement loading of a data instance from the server.
      • ZaItem.initMethods/Queues of methods that implement initialization.
      • ZaItem.createMethods. Queues of methods that implement creation of an instance on the server.
      • ZaItem.modifyMethods. Queues of methods that implement modification of a data instance on the server.
      • ZaItem.removeMethods. Queues of methods that implement removal of data instance from the server.

(sentence not complete)In each of these static maps a key is a name of the model class, e.g. “ZaAccount”, “ZaServer”, and a value is the array of function references, i.e. References to methods that implement loading of an Account instance from the server are stored in ZaItem.loadMethods[“ZaAccount”] array, and references to the methods that implement deletion of an Account instance from the server are stored in ZaItem.removeMethods[“ZaAccount”].

ZaItem.initMethods

Methods referenced in initMethods map are responsible for any post-constructor initialization. These methods are called from within ZaItem._init method which is called at the end of a constructor. ZaItem.initMethods is useful when extending an existing model class, because it allows adding statements that will be called at the end of the constructor call.

In the following example we add an initialization step to ZaServer object. The purpose of this additional step is to initialize two class members and assign default values. This example is the actual code from the com_zimbra_hsm admin extension which is available in the ZCS Network version. The purpose of the extension is to add tools for volume management to the Zimbra administration console.

Code Example

/**

* Declare the initialization method. Assume that ZaHSM constructor was 

* defined earlier.

* @param app – reference to an instance of the main application controller.

**/

ZaHSM.serverInit = function (app) {

  this.hsm = new Object();

  this.hsm.pollInterval = 500;

}

/**

* Add the new reference to the initialization method to the array of

* references in ZaItem.initMethods map under the key “ZaServer”.

**/

if(ZaItem.initMethods[”ZaServer”]) {

  ZaItem.initMethods[”ZaServer”].push(ZaHSM.serverInit);

}


ZaItem.loadMethods

As mentioned earlier, methods referenced in loadMethods map are responsible for loading data from the server. If you want to find out how these methods are called, look at ZaItem.load method in the ZaItem.js file. (do you need to tell them where to find it?)

Code example

In the following example we create a function that loads some data from the server via a SOAP request. Note that this example assumes that there is a SOAP Handler for URN “mytestURN” that is aware of the SOAP command “MyTestRequest” and returs (do you mean returns) “MyTestResponse” document in JSON format; “MyTestRequest” SOAP command takes one argument “testArgument“.

//declare the method

MyTestClass.loadMethod = function(by, val) {

  if(!val)

    return;

  var soapDoc = AjxSoapDoc.create(”MyTestRequest”, “urn:myTestURN”, null);

  soapDoc.set(”testArgument”, “testValue”);

  var getAdditionalInfoCommand = new ZmCsfeCommand();

  var params = new Object();

  params.soapDoc = soapDoc;

  var resp =   getAdditionalInfoCommand.invoke(params).Body.MyTestResponse.myTestObj[0];

  this.initFromJS(resp);

}

/**

*  add the method reference to the queue of method references in  

* ZaItem.loadMethods map. “ZaAccount” key corresponds to methods that

*  are called when loading a ZaAccount object

**/

if(ZaItem.loadMethods[”ZaAccount”]) {

  ZaItem.loadMethods[”ZaAccount”].push(MyTestClass.loadMethod);

}

ZaItem.createMethods

Methods referenced in createMethods map are responsible for creating a data instance on the server. If you want to find out how these methods are called, look at ZaItem.create method in ZaItem.js file. In the ZCS administration console, every type of server object has a corresponding JavaScript class, e.g. Instances of Account objects stored in LDAP database on the server are represented as instanced of ZaAccount JavaScript class on the client side. Because multiple methods will be called in the UI when the UI creates an Account object on the server, ZaItem.create method creates an empty instance of ZaAccount JavaScript class before calling all the methods in ZaItem.createMethods[”ZaAccount”] queue. The same is true for other object types (ZaServer, ZaCos, ZaResource, ZaDomain, etc).

Code example

In the following example, we create a function that makes modifications to an Account object on the server after the object is created. This code example is the actual code from zimbra_samba extension that allows managing Samba accounts via Zimbra administration console.


/**

*Declare the function assuming that ZaSamAccount constructor was defined 

* earlier.

* @param tmpObj – a temporary javascript object that holds the data for the

* new instance of an Account object. TmpObj may or may not

* be an instance of ZaAccount JavaScript class.

* @param account – an instance of ZaAccount JavaScript class that was

* created in ZaItem.create method. At the time when

* ZaSamAccount.createMethod is called, account may already contain the data 

* from the Account object that was created on the server

* during the previous calls.

* @param app – reference to an instance of the main application controller.

**/

ZaSamAccount.createMethod = function(tmpObj, account, app) {

  if(tmpObj.attrs[ZaAccount.A_password] &&

     tmpObj.attrs[ZaAccount.A_password].length > 0) {

       var soapDoc = AjxSoapDoc.create(

         ”ModifyAccountRequest”, 

         “urn:zimbraAdmin”, 

         null);

       soapDoc.set(”id”, account.id);

       var attr = soapDoc.set(”a”, 

       ZaSambaUtil.hex_md4(tmpObj.attrs[ZaAccount.A_password]));

       attr.setAttribute(”n”, ZaSamAccount.A_sambaNTPassword);

       var modifyAccCommand = new ZmCsfeCommand();

       var params = new Object();

       params.soapDoc = soapDoc;

       resp = modifyAccCommand.invoke(params).Body.ModifyAccountResponse;

       account.initFromJS(resp.account[0]);

       account[ZaAccount.A2_confirmPassword] = null;

  }

}

/**

* add the method reference to the queue of method references in  

* ZaItem.createMethods map. “ZaAccount” key corresponds to methods that

*  are called when creatin an Account object on the server.

**/

if(ZaItem.createMethods[”ZaAccount”]) {

  ZaItem.createMethods[”ZaAccount”].push(ZaSamAccount.createMethod);

}

ZaItem.modifyMethods

Methods referenced in modifyMethods map are responsible for modifying a data instance on the server. If you want to find out how these methods are called, look at ZaItem.modify method in ZaItem.js file.

ZaItem.removeMethods

Methods referenced in removeMethods map are responsible for removing a data instance on the server. If you want to find out how these methods are called, look at ZaItem.remove method in ZaItem.js file.

myXModel

MyXModel is a static member of each model class. This member is a JavaScript object. It contains meta-data that describes the structure of the data encapsulated in the model class in the Xforms terms. MyXModel is used only by the Xforms engine to determine how to bind a data instance to form elements.

Communicating with a server via SOAP/JSON

As I mentioned earlier, model classes are responsible for communicating with a server. Although, there are cases when other types of objects send requests to a server and parse server responses. Communication with a server is done by sending SOAP requests and receiving responses in XML or JSON format. SOAP requests can be either synchronous or asynchronous. Zimbra AJAX Toolkit provides convenient APIs for sending and receiving SOAP/JSON requests via HTTP.

Views

Views are classes that define layout of objects on the screen and tie data with UI elements. In the admin console, there are two basic types of views: list views and Xform views. List views extend DwtListView class from Zimbra AJAX Toolkit and display a list of objects in a table with an optional tool bar. Xform views are based on Zimbra Xform framework and usually are used to display properties of a selected data object.

Navigation panel (aka Overview Panel)

Overview panel contains the navigation tree. Nodes of the tree are created by the Overview Panel Controller.

List views

The main purpose of a list view is to display lists of objects in a table-like layout. A list view consists of three elements: a list, a tool bar, and a pop-up menu. These elements are created by an instance of a controller class. MVC-addicts may argue that this is a wrong way to encapsulate functionality, and the right way would be to have a list view class be responsible for creating all of its elements and laying them out on the screen. (The following sentences are not clear, the sentence does not seem finished) While I agree that, as I mentioned earlier, we did not follow the MVC framework too closely on the client side and the role of controller classes is somewhat blurry. When you write extensions to the administration console you might as well avoid controllers.

Examples of list view classes in ZCS administration console are ZaAccountListView, ZaServerListView, ZaDomainListView, etc. All list view classes in the administration UI inherit from ZaListView, which in term inherits from DwtListView. If you are adding your own list view class, it is recommended that you follow this inheritance pattern, although that is not required as long as your class implements the required methods. However, it is required that any view class inherits from DwtComposite class. Here is one way to think about it for those of you who are more familiar with languages like C++ and Java. If JavaScript had abstract classes and pure abstract classes like C++ or interfaces like Java, there would have been a common interface (pure abstract class), e.g. IlistView that every list view class had to implement. DwtListView would have been a default implementation of that interface and would extend DwtComposite class. Since JavaScript does not enforce inheritance natively, any such enforcement would have been artificial and limiting. Therefore, the framework does not enforce the inheritance, but only advises it.

Methods that a list view class has to implement:

      • getTabIcon
      • getTabTitle
      • _sortColumn
      • _setNoResultsHtml
      • _createItemHtml

Following is an example of a simple list view class.

Code example

In the following example, we create a list view class that overwrites two list view methods toString and getTitle.


/**

* @constructor

* @class ZaMyTestListView

* @param parent

**/

ZaMyTestListView= function(parent) {

  ZaListView.call(this, parent, null, DwtControl.ABSOLUTE_STYLE);

}

ZaMyTestListView.prototype = new ZaListView;

ZaMyTestListView.prototype.constructor = ZaMyTestListView;

ZaMyTestListView.prototype.toString = function() {

  return “ZaMyTestListView”;

}

ZaMyTestListView.prototype.getTitle = function () {

  return “My Test View”;

}

Form views

Most form views display properties of a selected data object such as a server or an account. Therefore, form views consist of a tool bar, a tab bar and several HTML forms placed on tab cards. Similar to list views, form view elements are created by an instance of a controller class. All form views inherit from ZaTabView. If you are creating your own form views, you are not required to inherit your view classes from ZaTabView, but it is required that any view class inherits from DwtComposite class. In order to build forms quickly and consistently, we created a special framework for rendering and handling forms and called it Xforms in analogy to W3C Xforms proposal.

The following methods are common for all classes that implement form views: setObject, getTitle and myXFormModifier.setObject method takes an instance of a model class as its argument, caches the data of the instance in a member variable and passes the data to the Xforms form that is displayed on the view. In order to construct a form, we have to provide the Xforms framework with two meta-data objects, one that describes the data model for the form and another that describes the form elements and layout. The object that describes the data model is myXModel object described in section [#_toc155 #3.2.6.myXModel]. The object that describes form elements and layout is accessed by getMyXForm method of a view class. Default implementation of getMyXForm method is available in ZaTabView class, and it is not recommended to overwrite this method. The getMyXForm method follows the mechanism similar to the mechanism of initializing, loading, and modifying data instances as described in section [#_toc23 #3.2.Models]. There is a static map ZaTabView.XformModifiers where keys are names of the view classes and values are arrays of references to methods that define elements of a form, e.g. All methods that define form elements for a form that displays an account object are referenced in ZaTabView.XformModifiers[“ZaAccountXFormView”] array.

Following is an example of a simple form view class. This is an actual form view class for a view that manages Samba Domains in zimbra_samba admin extension.

Code example


/**

* @class ZaSambaDomainXFormView

* @contructor

* @param parent

* @param app

**/

function ZaSambaDomainXFormView (parent, app) {

  /* call the parent constructor, “ZaSambaDomainXFormView” is the name of 

  the CSS class for this view */

  ZaTabView.call(this, parent, app,”ZaSambaDomainXFormView”);

  /* initialize the Xform */

  this.initForm(ZaSambaDomain.myXModel,this.getMyXForm()); 

  /* assign a reference to the main application controller to the Xform 

  instance, so that elements on the form can access the main application 

  controller */

  this._localXForm.setController(this._app); 

}

ZaSambaDomainXFormView.prototype = new ZaTabView();

ZaSambaDomainXFormView.prototype.constructor = ZaSambaDomainXFormView;

ZaTabView.XformModifiers[”ZaSambaDomainXFormView”] = new Array();

/**

* @param entry – an instance of  ZaSambaDomain class, that contains data 

* for a Samba Domain object

**/

ZaSambaDomainXFormView.prototype.setObject = function (entry) {

  this._containedObject.attrs = new Object();

  /* create an object to store the local cache of the data */

  this._containedObject = new ZaSambaDomain(this._app); 

  /**

  * BEGIN saving local copy of the data

  **/

  if(entry.id)

    this._containedObject.id = entry.id;

  if(entry.name)

    this._containedObject.name = entry.name;

  for (var a in entry.attrs) {

    if(entry.attrs[a] instanceof Array) {

      this._containedObject.attrs[a] = new Array();

      var cnt = entry.attrs[a].length;

      for (var ix = 0; ix < cnt; ix++) {

        this._containedObject.attrs[a][ix]=entry.attrs[a][ix];

      }

    } else {

      this._containedObject.attrs[a] = entry.attrs[a];

    }

  }

  /**

  * END saving local copy of the data

  **/

  /**

  * initialize the variable which is not part of  ZaSambaDomain class, but 

  * is used by the form

  **/

  if(!entry[ZaModel.currentTab])

    this._containedObject[ZaModel.currentTab] = “1”;

  else

    this._containedObject[ZaModel.currentTab] = entry[ZaModel.currentTab];

  //pass the data to the form

  this._localXForm.setInstance(this._containedObject);

  //trigger form redraw

  this.updateTab();

}

ZaSambaDomainXFormView.prototype.getTitle = function () {

  return “Samba Domains”;

}

/**

* Define a static method which defines form elements

* @param xFormObject – a reference to the object that contains definitions 

* of form elements. This object was created in 

* ZaTabView.prototype.getMyXForm prior to calling this method

**/

ZaSambaDomainXFormView.myXFormModifier = function(xFormObject) {

  xFormObject.tableCssStyle=”width:100%;overflow:auto;”;

  xFormObject.items = [

    {type:_GROUP_, 

      cssClass:”ZmSelectedHeaderBg”, 

      colSpan: “*”, 

      id:”xform_header”,

      items: [

        {type:_GROUP_,

          numCols:4,

          colSizes:[”32px”,”350px”,”100px”,”250px”],

          items: [

            {type:_AJX_IMAGE_, src:”Domain_32”, label:null},

            {type:_OUTPUT_, 

              ref:ZaSambaDomain.A_sambaDomainName, 

              label:null,cssClass:”AdminTitle”, rowSpan:2},

            {type:_OUTPUT_, ref:ZaSambaDomain.A_sambaSID, label:”sambaSID”}

          ]

        }

      ],

      cssStyle:”padding-top:5px; padding-left:2px; padding-bottom:5px”

    },

    {type:_TAB_BAR_, 

      ref:ZaModel.currentTab,

      choices:[

        {value:1, label:ZaMsg.Domain_Tab_General}

      ],

      cssClass:”ZaTabBar”, id:”xform_tabbar”

    },

    {type:_SWITCH_,

      items:[

        {type:_ZATABCASE_, 

          relevant:”instance[ZaModel.currentTab] == 1”,

          colSizes:[”250px”,”*”],

          items:[

            {ref:ZaSambaDomain.A_sambaDomainName, 

              type:_TEXTFIELD_,

              label:ZaMsg.Domain_DomainName,

                   onChange:ZaTabView.onFormFieldChanged},

            {ref:ZaSambaDomain.A_sambaSID, 

              type:_TEXTFIELD_,

              label:”sambaSID”, width:300,

                   onChange:ZaTabView.onFormFieldChanged},

            {ref:ZaSambaDomain.A_sambaAlgorithmicRidBase, 

              type:_TEXTFIELD_,

              label:”sambaAlgorithmicRidBase”, 

              cssClass:”admin_xform_number_input”,

              onChange:ZaTabView.onFormFieldChanged}]

        }

]

         }

  ];

}

/* put the reference to the method defined above into the static map with the key that corresponds to this view */

ZaTabView.XformModifiers[”ZaSambaDomainXFormView”].push(ZaSambaDomainXFormView.myXFormModifier); 

ZaTabView.XFormModifiers

Methods referenced in ZaTabView.XformModifiers map are responsible for defining meta-data that describes form elements and form layout in Xforms syntax. If you want to find out how these methods are called, look at ZaTabView.getMyXForm method in ZaTabView.js file.

ZaTabView.getMyXForm vs. ZaItem.myXModel

A question you may have after reading the previous section is how does meta data returned by ZaTabView.getMyXForm interact with meta data in ZaItem.myXModel. Lets look at an example. Let’s say, we have an Account object to be rendered. ZaItem.myXModel will say that the account has a name, which is a string, and a quota which is an integer. The ZaItem.myXModel may also say that an account may have a Class of Service, which is an ID of a Class of Service object, and if the quota attribute of the account is NULL, than it defaults to the quota attribute in the account’s class of service. Metadata returned by ZaTabView.getMyXForm will say that the account’s name is to be rendered as an editable text box, and Class of Service is to be rendered as a drop down with three values to select from.

Controllers

If you have read the sections about Models and Views, then by now you are probably wondering how Views get the Data and how user actions translate into data changes. This is exactly what controllers’ role is. In the administration console, controllers are the glue between the views and the data. Controllers pass data from Models to Views and vice versa. There are four types of controllers in the administration console: list view controllers, form view controllers, main application controller, and overview panel controller. The last two controllers are singletons, and therefore there is only one instance of the main application controller and one instance of the overview panel controller per application. The base class for all controllers is ZaController. This class provides default implementations of several methods and holds maps of references to methods (similar to ZaTabView.XformModifiers, ZaItem.loadMethods and other maps of references discussed previously). The following maps of method references are important when extending existing views and creating new views:

  • ZaController.initToolbarMethods
  • ZaController.initPopupMenuMethods

ZaController.initToolbarMethods

Methods referenced in ZaController.initToolbarMethods map are responsible for adding buttons to toolbars. Similar to all other reference maps, keys in the map are names of controllers and values are arrays of references to methods. Methods referenced in this map are called by ZaController.prototype._initToolbar

Following is an example of using to add buttons to the toolbar. This example is taken from com_zimbra_cert_manager extension.

Code example

/**

* Method that adds button descriptors to this view’s toolbar

**/

ZaCertsServerListController.initToolbarMethod = function () {

  this._toolbarOperations.push(

    new ZaOperation(ZaOperation.VIEW,

    com_zimbra_cert_manager.TBB_view_cert,

    com_zimbra_cert_manager.TBB_view_cert_tt, 

    “ViewCertificate”, 

    “ViewCertificate”, 

     new AjxListener(

       this,

       ZaCertsServerListController.prototype.viewCertListener)

     )

  );

  this._toolbarOperations.push(

    new ZaOperation(ZaOperation.NEW,

    com_zimbra_cert_manager.TBB_launch_cert_wizard,

    com_zimbra_cert_manager.TBB_launch_cert_wizard_tt,

    “InstallCertificate”,

    “InstallCertificate”,

    new AjxListener(this,

      ZaCertsServerListController.prototype._newCertListener)

    )

  );

  this._toolbarOperations.push(new ZaOperation(ZaOperation.NONE));

  this._toolbarOperations.push(

    new ZaOperation(ZaOperation.HELP, 

      com_zimbra_cert_manager.TBB_Help, 

      com_zimbra_cert_manager.TBB_Help_tt,

      “Help”,

      “Help”, 

      new AjxListener(this, this._helpButtonListener)

    )

  );

}

//add method reference to the map

ZaController.initToolbarMethods[”ZaCertsServerListController”].push(

  ZaCertsServerListController.initToolbarMethod);

ZaController.initPopupMenuMethods

This map is very similar to the initToolbarMethods map. Methods referenced in this map define items that appear in the popup menu. Most of the times, items in the popup menu mirror toolbar buttons. Methods referenced in this map are called by ZaController.prototype._initPopupMenu.

_createUI method

When you are adding a new view to the administration console, there are two methods that you have to implement: _createUI, and show. _createUI method is responsible for instantiating elements of the view and calling the view manager to lay out the elements on the screen.

Following is an example of _createUI method of a form view controller class in com_zimbra_cert_manager extension which implements certificate management. In this example, _createUI method creates a toolbar, and a form view.

Code example

ZaCertViewController.prototype._createUI = function () {

  try {

    //map of elements that constitute the view

    var elements = new Object();

    //instantiate the form view

    this._contentView = new ZaCertView( this._container, this._app );

    /* initialize toolbar buttons, this calls 

    * ZaController.prototype._initToolbar method which will iterate 

    * through all the methods referenced in

    * ZaController.initToolbarMethods[”ZaCertViewController”] array */

    this._initToolbar();

    if(this._toolbarOperations && this._toolbarOperations.length) {

      //instantiate the toolbar

      this._toolbar = new ZaToolBar(

        this._container,    

        this._toolbarOperations

      );

      //add the toolbar to the map

      elements[ZaAppViewMgr.C_TOOLBAR_TOP] = this._toolbar;

    }

    //add the xform view to the map

    elements[ZaAppViewMgr.C_APP_CONTENT] = this._contentView;

    var tabParams = {

      openInNewTab: true,

      tabId: this.getContentViewId(),

      tab: this.getMainTab()

    }

    //call view manager to layout all the elements on the screen     

    this._app.createView(this.getContentViewId(), elements, tabParams);

    this._UICreated = true;

    this._app._controllers[this.getContentViewId ()] = this ;

  } catch (ex) {

      this._handleException(ex, 

        “ZaCertViewController.prototype._createUI”, 

        null, 

        false

      );

      return;

       }

}

Following is an example of _createUI method of a list view controller class in com_zimbra_cert_manager extension. In this example, _createUI method creates a toolbar, a popup menu, and a list view.

Code example

ZaCertsServerListController.prototype._createUI = function () {

  try {

    var elements = new Object();

    this._contentView = new ZaCertsServerListView(this._container);

  /* this will call ZaController.prototype._initToolbar method, which will 

  * iterate through methods referenced in 

  * ZaController.initToolbarMethods[“ZaCertsServerListController”] array */

    this._initToolbar();

    if(this._toolbarOperations && this._toolbarOperations.length) {

      this._toolbar = new ZaToolBar(

        this._container, 

        this._toolbarOperations

      );

      elements[ZaAppViewMgr.C_TOOLBAR_TOP] = this._toolbar;

}

/* this will call ZaController.prototype._initPopupMenu method, which will * iterate through methods referenced in 

* ZaController.initPopupMenuMethods[“ZaCertsServerListController”] array */

    this._initPopupMenu();

    if(this._popupOperations && this._popupOperations.length) {

      this._actionMenu = 

        new ZaPopupMenu(

          this._contentView, 

            “ActionMenu”, 

            null, 

            this._popupOperations

        );

    }

    elements[ZaAppViewMgr.C_APP_CONTENT] = this._contentView;

    var tabParams = {

      openInNewTab: false,

      tabId: this.getContentViewId(),

      tab: this.getMainTab()

    }

    //call view manager to layout all the elements on the screen

    this._app.createView(this.getContentViewId(), elements, tabParams);

    this._contentView.addSelectionListener(

      new AjxListener(this, this._listSelectionListener)

    );

    this._contentView.addActionListener(

      new AjxListener(this, this._listActionListener)

    );

    this._removeConfirmMessageDialog = 

      new ZaMsgDialog(

        this._app.getAppCtxt().getShell(), 

        null, 

        [DwtDialog.YES_BUTTON, DwtDialog.NO_BUTTON], 

        this._app

      );

    this._UICreated = true;

    this._app._controllers[this.getContentViewId ()] = this ;

  } catch (ex) {

      this._handleException(

        ex, 

        “ZaCertsServerListController.prototype._createUI”, 

        null, 

        false

      );

      return;

  }

    }  

show method

This method is called to show a view. E.g.: when a user selects “Servers” node in the navigation tree, an instance of ZaOverviewPanelController will call ZaServerListController.prorotype.show method. This method’s responsibility is to call _createUI method, pass the data to view, and call the view manager to make the view visible. The method has two arguments. In case of list view controller, the first argument is the list of objects to be displayed in the list view, and the second argument is a Boolean flag that tells whether to create a new tab for the view.

Following is an example of show method of a list view controller class in com_zimbra_cert_manager extension.

Code example

/**

* @param list {ZaItemList} a list of ZaServer {@link ZaServer} objects

* @param openInNewTab {Boolean} a flag that indicates whether to open the

* view in the new tab

**/

ZaCertsServerListController.prototype.show = function(list, openInNewTab) {

  if (!this._UICreated) {

    //create the UI components

    this._createUI();

  } 

  //pass the data to the view

  if (list != null)

    this._contentView.set(list.getVector());

  //tell the view manager to show the view

  this._app.pushView(this.getContentViewId());

  if (list != null)

    this._list = list;

  this.changeActionsState();

}

In case of form view controller, the first argument is the object to be shown on the form, and the second argument is the id of the tab where the view will be displayed.

Following is an example of show method of a form view controller class in com_zimbra_cert_manager extension.


Code example


ZaCertViewController.prototype.show = function(certs, targetServerId) {

  if (!this._UICreated) {

    this._createUI();

  } 

  if (certs != null) {

    this._contentView.set(certs, targetServerId);

  }

  this._app.pushView(this.getContentViewId());

}

Handling exceptions

ZaController class has a helper method for handling and reporting exceptions: ZaController.prototype._handleException method. This method allows the caller to display an error message, restart the application and call a function after the application is restarted.The method takes five arguments:

  1. Exception object
  2. Name of the function where the exception was caught (String value) or a reference to the function which should be executed after the application is restarted
  3. Array of arguments for the function that will be executed when the application is restarted
  4. Boolean flag that indicates whether the application needs to be restarted
  5. The object to be used as the context of the function to be called after the application is restarted

Only the first argument is required, and the rest of the arguments are optional. It is generally not recommended to force the application to restart when an exception is caught. In fact, there is only one place in the code of Zimbra Administration Console that uses TRUE for the fourth argument to ZaController.prototype._handleException call.

When the fourth argument (boolean flag) is FALSE, _handleException method will show an error dialog with the details of the exception. For well-known exceptions, such as authentication failure, network error, the dialog will have a customized user-friendly message. If the server's stack trace is available for the exception it will be displayed in the error dialog.

List view controllers

As the title suggests, list view controllers control list views. The responsibilities of a list view controller are:

    • instantiate and initialize the elements: toolbar, popup menu, list view
    • capture and dispatch user-generated events: button click in the toolbar, element selection in the list view, element selection in the popup menu, and double click in the list view
    • control the state of the buttons in the toolbar and elements in the popup menu. E.g.: determine whether a particular button should be enabled or disabled and whether a particular element in the popup menu should be enabled or disabled
    • handle some of the events generated by changes in data. E.g.: redraw the list view when an object is deleted from the view

The framework of the administration console provides a base class for list view controllers: ZaListViewController which inherits from ZaController class. ZaListViewController holds some additional method maps and provides default implementations for methods commonly used by list view controllers.

Form view controllers

Form view controllers control XForm views. The responsibilities of a form view controller are:

  • instantiate and initialize the elements: toolbar and form view
  • cache the data instance and pass it on to the form view
  • capture and dispatch button clicks in the toolbar
  • control the state of buttons in the toolbar. E.g.: enable “save” button if the user changes anything on the form
  • determine what data is modified and what data is unchanged, and call ZaItem.modify method to submit the changes to server

Main application controller

The main application controller is implemented by ZaApp class. The only thing you really need to know about ZaApp is that it holds references to the instances of all other controllers and provides several utility methods.

Overview panel controller

ZaOverviewPanelController instantiates ZaOverviewPanel class, creates nodes in the navigation panel and handles selection of the nodes.

Message dialogs

Zimbra administration console uses message dialogs to show messages and prompt the user with Yes/No questions.

XForm Dialogs

XForm dialogs are similar to message dalogs. The only difference is that an XForm dialog uses XForm’s framework to draw the contents of the dialog.

Wizard Dialogs

Wizard dialog is a form of XForm dialog. In addition to functionality provided by an XForm dialog, a Wizard dialog also provides methods for navigating a step-by-step wizard.

Application Skins

TBD

Extensions for Zimbra Administration Console

What is an extension for Zimbra administration console

Thanks to the flexibility of JavaScript, it is possible to modify nearly every aspect of the administration console through the mechanism of extensions. The framework allows developers to add new views to the administration console, manage new data objects in the administration console, extend existing objects with new properties, and customize existing views. Here are some examples of what can be done through this mechanism:

  • Integrate Zimbra with Samba and manage user and machine accounts through Zimbra administration console
  • Manage SSL certificates though Zimbra administration console
  • Expose Webmin, Plesk, cPanel and WHM Control Panels functionality through Zimbra administration console

In this chapter I will provide examples of extensions to the administration console.

Understanding the components of an extension

An extension for Zimbra administration console consists of a UI component and an optional server component.

The UI component consists of the following files:

  • Extension manifest
    An XML file that describes the extension to the framework. The file should has the same name as the extension. I.e. for zimbra_samba extension, the XML file is zimbra_samba.xml.
  • Configuration template
    An XML file that describes configuration properties and default values.
  • *.properties file with localized string constants. This file has the same nae as the extension. I.e. for com_zimbra_backuprestore extension's strings file is com_zimbra_backuprestore.properties.
  • JavaScript files

The Server component of the extension may follow the server extension framework, but this is not a requirement. The server component can be a server-side script, or a servlet hosted on the same or on a different server.

Loading extensions

Extensions are loaded by the administration console UI after user authentication. If you want to know exactly how the process works, take a look at ZaSettings.init method.

First, the framework looks for the resource file that contains localized string constants. The resource file is generated at runtime at /zimbraAdmin/res/name of the extension.js. I.e. if the extension is zimbra_samba, then the resource file will be /zimbraAdmin/res/zimbra_samba.js. We will discuss resource files later in this chapter.

Next, the framework loads all the JavaScript files that are included in the extension. JavaScript files are loaded by dynamically creating <script> tags to the document. An important consequence of this method of loading extensions is that after each javaSript file is loaded, all statements in the file are executed by the browser.


Creating content of an extension

In this guide, we will focus on the UI component and the most generic and common uses of the admin extension framework:

  • adding new views
  • modifying existing views
  • adding data objects
  • extending existing data objects

Adding a list view

As you can see from the description of the framework, in order to add a list view to the Zimbra administration console, you need to create a list view class, and a list view controller class. Once the classes are implemented, you need a way to tell the administration console when to show the new list view. The easiest way to do this is to add a node to the navigation tree (aka Overview Panel) and show the new list view when the node is selected.

Creating a list view class

Lets consider the following task for this example. We want to retrieve a list of sambaDomain objects from an LDAP server and show the list in the administration console. List views are described previously in details in the section List Views. Here, I will give an example of a real list view class which is used in zimbra_samba extension.

Code example

function ZaSambaDomainListView(parent)  {

  var className = null;

  var posStyle = DwtControl.ABSOLUTE_STYLE;

  var headerList = this._getHeaderList();

  ZaListView.call(this, parent, className, posStyle, headerList);

  this._appCtxt = this.shell.getData(ZaAppCtxt.LABEL);

  this.setScrollStyle(DwtControl.SCROLL);

}

ZaSambaDomainListView.prototype = new ZaListView;

ZaSambaDomainListView.prototype.constructor = ZaSambaDomainListView;

ZaSambaDomainListView.prototype.toString = function() {

  return “ZaSambaDomainListView”;

}

ZaSambaDomainListView.prototype.getTitle = function () {

  return “Manage Samba Domains”;

}

/**

* Renders a single item as a DIV element.

*/

ZaSambaDomainListView.prototype._createItemHtml = 

function(object, now, isDndIcon) {

  var html = new Array(50);

  var div = document.createElement(”div”);

  div[DwtListView._STYLE_CLASS] = “Row”;

  div[DwtListView._SELECTED_STYLE_CLASS] = 

    div[DwtListView._STYLE_CLASS] + “-” + DwtCssStyle.SELECTED;

  div.className = div[DwtListView._STYLE_CLASS];

  this.associateItemWithElement(object, div, DwtListView.TYPE_LIST_ITEM);

  var idx = 0;

  html[idx++] = “<table width=’100%’ cellspacing=’2’ cellpadding=’0’>”;

  html[idx++] = “<tr>”;

  var cnt = this._headerList.length;

  for (var i = 0; i < cnt; i++) {

    var id = this._headerList[i]._id;

    if(id.indexOf(ZaSambaDomain.A_sambaSID)==0) {

      // name

      html[idx++] = 

        “<td align=’left’ width=” + this._headerList[i]._width + “><nobr>”;

      html[idx++] = 

        AjxStringUtil.htmlEncode(object.attrs[ZaSambaDomain.A_sambaSID]);

      html[idx++] = “</nobr></td>”;

    } else if (id.indexOf(ZaSambaDomain.A_sambaDomainName)==0) {

      // description

      html[idx++] = 

        “<td align=’left’ width=” + this._headerList[i]._width + “><nobr>”;

      html[idx++] = 

         AjxStringUtil.htmlEncode(

          object.attrs[ZaSambaDomain.A_sambaDomainName]);

      html[idx++] = “</nobr></td>”;

    }

    html[idx++] = “</tr></table>”;

    div.innerHTML = html.join(””);

    return div;

  }

    }    

    ZaSambaDomainListView.prototype._getHeaderList = function() {

      var headerList = new Array();

      var sortable=1;

      headerList[0] = 

        new ZaListHeaderItem(

          ZaSambaDomain.A_sambaDomainName, 

          “Domain Name”, 

          null, 

          200, 

          null, 

          ZaSambaDomain.A_sambaDomainName, 

          true, 

          true);

      headerList[1] = 

   new ZaListHeaderItem(

     ZaSambaDomain.A_sambaSID, 

     “sambaSID”, 

     null, 

     null, 

     null, 

     ZaSambaDomain.A_sambaSID,

     true,

     true);

      return headerList;

    }    

Constructing the view object

In most cases, the constructor of any view class will look like the constructor in this example. All the constructor does is initializing default values and calling the parent constructor. When creating a list view class, you can always use the same code for the constructor.

Next two lines after the constructor defines the inheritance of the class

ZaSambaDomainListView.prototype = new ZaListView; ZaSambaDomainListView.prototype.constructor = ZaSambaDomainListView; 

All list view classes must have ZaListView as their prototype. However, javascript does not have a built-in mechanism to restrict inheritance; and therefore, nothing prevents you from creating a list view class that does not have ZaListView class as a prototype. As long as the list view class correctly implements all the methods of ZaListView and DwtListView classes, it will work.

Setting the title of the list view

The next method ZaSambaDomainListView.prototype.getTitle is called by the view manager when the user switches to this list view. String returned by this method will be displayed in the address area. In the screen shot below, getTitle method returned “Manage Accounts” string when I clicked on Accounts in the navigation panel.


[[Image:]]


The string that is returned by the getTitle method can be localized by using *.properties files.

Rendering list items

The next method ZaSambaDomainListView.prototype._createItemHtml is called by the parent class DwtListView when it renders the list. This method has to return a DIV object, which has to be created by calling document.createElement(“div”). DwtListView calls _createItemHtml method for each item that has to be displayed in the list, and places DIVs returned by _createItemHtml on the list view. In order to your list look similar to other lists in the administration console, we should use the same CSS classes that are used in this example:

div[DwtListView._STYLE_CLASS] = “Row”;

div[DwtListView._SELECTED_STYLE_CLASS] = div[DwtListView._STYLE_CLASS] + “-” + DwtCssStyle.SELECTED;

div.className = div[DwtListView._STYLE_CLASS];

That will ensure that the list looks like other lists and that selected list elements are properly highlighted.

The next statement is important if you plan to implement actions for list elements, such as opening an object in a form view to show more details or edit, deleting an object, etc.

this.associateItemWithElement(object, div, DwtListView.TYPE_LIST_ITEM); 

Next, in this example we iterate through  this._headerList to output each cell of the column in the row. this._headerList is defined in the next method   

ZaSambaDomainListView.prototype._getHeaderList.


This method returns an array of ZaListHeaderItem objects which are used to create the list header and define columns in the list view. If your list view has only one column, you do not have to implement this method.

Creating a list view controller class

I will continue with the same example, showing a list of sambaDomain objects. If you look at the actaual ZaSambaDomainListController class, it has more functionality then what I will describe here. For simplicity, lets narrow the scope and assume that the tool bar for this list view has only one button: “Edit”, which will open the Xform view with details of the selected sambaDomain.

Code example

function ZaSambaDomainListController(appCtxt, container, app) { 

  ZaListViewController.call(this, 

  appCtxt, 

  container, 

  app,

  ”ZaSambaDomainListController”);

}

ZaSambaDomainListController.prototype = new ZaListViewController();

ZaSambaDomainListController.prototype.constructor = ZaSambaDomainListController;

ZaController.initToolbarMethods[”ZaSambaDomainListController”] = new Array();

ZaSambaDomainListController.prototype.show = function(list) {

  if (!this._UICreated) {

    this._createUI();

  } 

  if (list != null)

    this._contentView.set(list.getVector());

  this._app.pushView(this.getContentViewId());

  if (list != null)

    this._list = list;

}

ZaSambaDomainListController.initToolbarMethod = function () {

  this._toolbarOperations.push(

    new ZaOperation(

      ZaOperation.EDIT, 

      ZaMsg.TBB_Edit, 

      ZaMsg.SERTBB_Edit_tt, 

      “Properties”, 

      “PropertiesDis”, 

      new AjxListener(this,

        ZaSambaDomainListController.prototype._editButtonListener)

      )

    );

}

ZaController.initToolbarMethods[”ZaSambaDomainListController”].push(

ZaSambaDomainListController.initToolbarMethod);

ZaSambaDomainListController.prototype._createUI = function () { 

  try {

    var elements = new Object();

    this._contentView = new ZaSambaDomainListView(this._container);

    this._initToolbar();

    if(this._toolbarOperations && this._toolbarOperations.length) {

      this._toolbar = new ZaToolBar(

                        this._container, 

                        this._toolbarOperations);

      elements[ZaAppViewMgr.C_TOOLBAR_TOP] = this._toolbar;

    }

    elements[ZaAppViewMgr.C_APP_CONTENT] = this._contentView;

    var tabParams = {

      openInNewTab: false,

      tabId: this.getContentViewId(),

      tab: this.getMainTab()

         }

    this._app.createView(this.getContentViewId(), elements,tabParams);

    this._contentView.addSelectionListener(

      new AjxListener(this,this._listSelectionListener)

    );

    this._contentView.addActionListener(

      new AjxListener(this, this._listActionListener)

    );

    this._UICreated = true;

  } catch (ex) {

    this._handleException(ex,

      “ZaSambaDomainListController.prototype._createUI”, 

      null, 

      false

    );

    return;

  }

}

Constructing a list view controller object

The constructor is very simple, it has only one statement, which is to tall the parent constructor. Following the constructor are two statements that implement inheritance in JavaScript. The following statement is necessary only if you want your view to have a tool bar.

Creating contents of a list view

Method ZaSambaDomainListController.prototype._createUI instantiates elements of the UI: tool bar and list view. After being instantiated, these elements are assigned to the main application controller which takes care of the layout

this._app.createView(this .getContentViewId(), elements,tabParams); 

Object elements is a map where keys correspond to a predefined position of the element on the screen: ZaAppViewMgr.C_APP_CONTENT for the main content area and ZaAppViewMgr.C_TOOLBAR_TOP for the tool bar area. When we call this._app.createView method, layout manager arranges the elements on the screen according to the application skin.

Showing the list view

Next method is ZaSambaDomainListController.prototype.show. As the name suggests, this method is responsible for showing the view. The only argument that this method takes is the data object. There are no restrictions to what the data object is. In this case, it is an instance of ZaList. This statement this._contentView.set(list.getVector()); passes the data to the instance of the list view class. The following statement this._app.pushView(this.getContentViewId()); calls the main application controller's method “pushView” to push the DIV that contains this view to the top of the stack of divs that contain views, which makes the view visible.

Defining a tool bar for a list view

ZaController.initToolbarMethods[”ZaSambaDomainListController”] = new Array();

This statement initializes the array of method references in ZaController.initToolbarMethods map with the key that corresponds to this controller class. The key should be the same value that we passed to the parent constructor call in the constructor.

A tool bar is created by the ZaController.prototype._initToolbar method based on the this._toolbarOperations array. The array consists of ZaOperation objects. Each ZaOperation object describes one button in a tool bar.

Code example

this._toolbarOperations.push(

    new ZaOperation(

    ZaOperation.EDIT,

    ZaMsg.TBB_Edit,

    ZaMsg.SERTBB_Edit_tt,

    “Properties”,

    “PropertiesDis”,

    new AjxListener(this,

      ZaSambaDomainListController.prototype._editButtonListener)

    )

);  

ZaController.prototype._initToolbar calls methods referenced in ZaController.initToolbarMethods["ZaSambaDomainListController"] array. In this example we add a reference to ZaSambaDomainListController.initToolbarMethod method to this array.

Registering the list view controller

Before we add a handle to the new view to the navigation panel, we need to register the new controller with the main application controller (ZaApp). This can be done by adding a getter method to ZaApp class.

Code example

ZaZimbraAdmin._SAMBA_DOMAIN_LIST = ZaZimbraAdmin.VIEW_INDEX++;

     ZaApp.prototype.getSambaDomainListController = function() {

  if (this._controllers[ZaZimbraAdmin._SAMBA_DOMAIN_LIST] == null) {

    this._controllers[ZaZimbraAdmin._SAMBA_DOMAIN_LIST] = 

      new ZaSambaDomainListController(

        this._appCtxt, 

        this._container, 

        this

         );

  }

  return this._controllers[ZaZimbraAdmin._SAMBA_DOMAIN_LIST];

}

A global constant ZaZimbraAdmin.VIEW_INDEX is a counter that keeps track of how many view controller classes exist in the application.

In ZCS 5.0 all list view controllers are singletons, although this design pattern is not enforced by the framework and will be hard (if at all possible) to enforce in JavaScript. Therefore, instead of usual implementation of singleton pattern, where we would have a private constructor and a getInstance method, we let ZaApp.prototype.getSambaDomainListController method take care of maintaining only one instance of the controller class. All other objects should use this method to access the list view controller.

Adding A Node to The Navigation Panel

ZaOverviewPanelController class has API hooks similar to ZaModel, ZaController, and ZaTabView classes. Adding a node to the navigation panel is done in two steps. First, implement a method which will takes the navigation tree object as an argument and adds the new node to the tree. Second, add a reference to this method to ZaOverviewPanelController.treeModifiers array. In this example, we want to add a single node which will trigger the new list view described earlier.

Code example

Zambra.ovTreeModifier = function (tree) {   

  if(ZaSettings.SYSTEM_CONFIG_ENABLED) { 

    this._sambaTi = new DwtTreeItem(this._configTi);   

    this._sambaTi.setText(”Samba Domains”); 

    this._sambaTi.setImage(”ZimbraIcon”);

    this._sambaTi.setData(

      ZaOverviewPanelController._TID, 

      ZaZimbraAdmin._SAMBA_DOMAIN_LIST

    );

    if(ZaOverviewPanelController.overviewTreeListeners) {

      ZaOverviewPanelController.overviewTreeListeners[ZaZimbraAdmin._SAMBA_DOMAIN_LIST] = Zambra.sambaDomainListTreeListener;

         }

       }

}

if(ZaOverviewPanelController.treeModifiers)

  ZaOverviewPanelController.treeModifiers.push(Zambra.ovTreeModifier);

Zambra.sambaDomainListTreeListener = function (ev) {  

  if(this._app.getCurrentController()) {

    this._app.getCurrentController().switchToNextView(

      this._app.getSambaDomainListController(),

      ZaSambaDomainListController.prototype.show, 

      ZaSambaDomain.getAll(this ._app)

    );

  } else {

    this._app.getSambaDomainListController().show(

      ZaSambaDomain.getAll(this ._app)

    );

  }

}

Adding a new tab to an existing form view

Adding a tab to an existing form view is the easiest task in creating an extension. All you needto do is to define the form elements for the new tab and add the tab to the tab bar. Following is a code snippet from zimbra_posixaccount extension that adds a tab to the Account view.

Code example

if(ZaTabView.XformModifiers[”ZaAccountXFormView”]) {

  zimbra_posixaccount.AccountXFormModifier = function (xFormObject) {

    /* find the SWITCH element which is the parent element for all tabs */

    var cnt = xFormObject.items.length;

    var i = 0;

    for (i = 0; i <cnt; i++) {

      if(xFormObject.items[i].type==”switch”)

        break; //index i now points to the SWITCH element

           }

      //find the index of the next tab

      var posixTabIx = ++this.TAB_INDEX;

      //tab bar is the element with index 1

      var tabBar = xFormObject.items[1];

      //add the new tab button to the tab bar

      tabBar.choices.push({value:posixTabIx, label:”Posix Account”});

      //define meta data for new form elements

      var posixAccountTab = {

        type:_ZATABCASE_, numCols:1,

        relevant:(”instance[ZaModel.currentTab] == “ + posixTabIx),

        items: [

               {type:_ZAGROUP_,

            items:[

             {ref:ZaPosixAccount.A_gidNumber, 

               type:_OSELECT1_, editable:false,

               choices:this._app.getPosixGroupIdListChoices(true), 

               msgName:”Posix group”,

               label:”Posix group”, 

               labelLocation:_LEFT_, 

               onChange:ZaTabView.onFormFieldChanged},

             {ref:ZaPosixAccount.A_gidNumber, 

               type:_TEXTFIELD_, 

               msgName:ZaPosixAccount.A_gidNumber,

               label:ZaPosixAccount.A_gidNumber, 

               labelLocation:_LEFT_, 

               onChange:ZaTabView.onFormFieldChanged,            

               cssClass:”admin_xform_number_input”},

             {ref:ZaPosixAccount.A_uidNumber, 

               type:_TEXTFIELD_, 

               msgName:ZaPosixAccount.A_uidNumber,

               label:ZaPosixAccount.A_uidNumber, 

               labelLocation:_LEFT_, 

               onChange:ZaTabView.onFormFieldChanged,  

               cssClass:”admin_xform_number_input”},

             {ref:ZaPosixAccount.A_homeDirectory, 

               type:_TEXTFIELD_, 

               msgName:ZaPosixAccount.A_homeDirectory,

               label:ZaPosixAccount.A_homeDirectory, 

               labelLocation:_LEFT_, 

               onChange:ZaTabView.onFormFieldChanged, 

               width:250},

            {ref:ZaPosixAccount.A_loginShell, 

              type:_TEXTFIELD_, msgName:ZaPosixAccount.A_loginShell,

              label:ZaPosixAccount.A_loginShell, 

              labelLocation:_LEFT_, 

              onChange:ZaTabView.onFormFieldChanged, 

              width:250}

              ]  

            }

          ]

        };

    //add the new tab to the list of tabs

   xFormObject.items[i].items.push(posixAccountTab);

      }

  ZaTabView.XformModifiers[”ZaAccountXFormView”].push(

    zimbra_posixaccount.AccountXFormModifier

  );

}

First, we define a modifier function

zimbra_posixaccount.AccountXFormModifier = function (xFormObject)


which will add new form elements to the existing form. Then we add the reference to the new modifier function to the array of modifier references for the Account view

ZaTabView.XformModifiers[”ZaAccountXFormView”].push(zimbra_posixaccount.AccountXFormModifier);

When the Account view is instantiated, all functions referenced in this array will be called.

The argument of the modifier function is an object that contains meta data for the XForm. The object is being passed to each modifier method.

Adding a Form View

Adding a form view is similar to adding a list view. You need to create a form view controller class, and a form view class, and then connect the form view controller either to a navigation tree or to a list view controller.

Creating a form view class

Following the same example, we will create a form view class for displaying and editing a sambaDomain object.

function ZaSambaDomainXFormView (parent, app) {

  ZaTabView.call(this, parent, app,"ZaSambaDomainXFormView");

  this.initForm(ZaSambaDomain.myXModel,this.getMyXForm());

  this._localXForm.setController(this._app);

}

ZaSambaDomainXFormView.prototype = new ZaTabView();

ZaSambaDomainXFormView.prototype.constructor = ZaSambaDomainXFormView;

ZaTabView.XFormModifiers["ZaSambaDomainXFormView"] = new Array();

ZaSambaDomainXFormView.prototype.setObject = 

function (entry) {

  this._containedObject = new ZaSambaDomain(this._app);

  this._containedObject.attrs = new Object();

  if(entry.id)

    this._containedObject.id = entry.id;

  if(entry.name)

    this._containedObject.name = entry.name;

  for (var a in entry.attrs) {

     if(entry.attrs[a] instanceof Array) {

       this._containedObject.attrs[a] = new Array();

       var cnt = entry.attrs[a].length;

       for (var ix = 0; ix < cnt; ix++) {

   this._containedObject.attrs[a][ix]=entry.attrs[a][ix];

       }

     } else {

       this._containedObject.attrs[a] = entry.attrs[a];

     }

   }

   if(!entry[ZaModel.currentTab])

     this._containedObject[ZaModel.currentTab] = "1";

    else

     this._containedObject[ZaModel.currentTab] = entry[ZaModel.currentTab];

   this._localXForm.setInstance(this._containedObject);

   this.updateTab();

}

ZaSambaDomainXFormView.prototype.getTitle = 

function () {

  return "Samba Domains";

}

ZaSambaDomainXFormView.myXFormModifier = function(xFormObject) {

  xFormObject.tableCssStyle="width:100%;overflow:auto;";

  xFormObject.items = [

    {type:_GROUP_,

      cssClass:"ZmSelectedHeaderBg",

      colSpan: "*",

      id:"xform_header", 

      items: [

{type:_GROUP_,

      numCols:4,

                colSizes:["32px","350px","100px","250px"],

 items: [

            {type:_AJX_IMAGE_,

             src:"Domain_32", 

             label:null},

                 {type:_OUTPUT_,

                  ref:ZaSambaDomain.A_sambaDomainName, 

                  label:null,

                  cssClass:"AdminTitle", 

                  rowSpan:2},        {type:_OUTPUT_, 

                  ref:ZaSambaDomain.A_sambaSID, 

                  label:"sambaSID"}

 ]

}

],

      cssStyle:"padding-top:5px; padding-left:2px; padding-bottom:5px"},

    {type:_TAB_BAR_, 

     ref:ZaModel.currentTab,

     choices:[{value:1, label:ZaMsg.Domain_Tab_General}],

     cssClass:"ZaTabBar", 

     id:"xform_tabbar"

    },

    {type:_SWITCH_, 

     items:[

     {type:_ZATABCASE_, 

      relevant:"instance[ZaModel.currentTab] == 1", 

      colSizes:["250px","*"],

      items:[

        {ref: ZaSambaDomain.A_sambaDomainName, 

         type:_TEXTFIELD_, 

         label:ZaMsg.Domain_DomainName,

         onChange:ZaTabView.onFormFieldChanged

        },

        {ref: ZaSambaDomain.A_sambaSID, 

         type:_TEXTFIELD_, 

         label:"sambaSID", 

         width:300,

         onChange:ZaTabView.onFormFieldChanged

        },

        {ref: ZaSambaDomain.A_sambaAlgorithmicRidBase, 

         type:_TEXTFIELD_, 

         label:"sambaAlgorithmicRidBase", 

         cssClass:"admin_xform_number_input",

         onChange:ZaTabView.onFormFieldChanged

        }

       ]

}

    ]

   }

  ];

}

ZaTabView.XFormModifiers["ZaSambaDomainXFormView"].push(ZaSambaDomainXFormView.myXFormModifier);

Constructing a form view object

The contructor in the previous example has three statements. A call to the parent contructor

ZaTabView.call(this, parent, app,"ZaSambaDomainXFormView");

in which we pass the name of the new class as to the fourth argument. A call to _initForm method.

this.initForm(ZaSambaDomain.myXModel,this.getMyXForm());

First argument in this statement is meta data that describes a ZaSambaDomain object, which will be displayed in this view; second argument is metadata that describes the elements of the form.

Third statement assigns a reference to the main application controller to the form instance.

this._localXForm.setController(this._app);

The purpose of this call is to let form elements access to utility methods of ZaApp instance.

Passing data to the form

setObject method is responsible for passing data to the form. This method is called by the controller. Following statement passes the data to the Xfrorm

this._localXForm.setInstance(this._containedObject);

In this example, we create a local copy of the data (this._containedObject) in the view instance before passing it on to the form in order to be able to compare the data to the original state after it is modified by the user. Then we pass the local copy of the data to the XForm.

Defining form fields

When we create a new form view, we define form fields similar to how we define them when modifying an existing form view. In order to define form fields, we need to create a modifier function

ZaSambaDomainXFormView.myXFormModifier = function(xFormObject)

and add a reference to this function to the appropriate array of function references.

ZaTabView.XFormModifiers["ZaSambaDomainXFormView"].push(ZaSambaDomainXFormView.myXFormModifier);

Because this is a new view, the array of references is not initialized yet, so in order to avoid a null-reference error, we need to create the array before we push any references in it. A good practice is to define the array right after the constructor definitions at the top of the file

ZaTabView.XFormModifiers["ZaSambaDomainXFormView"] = new Array();

Creating a form view controller class

It is recommended that a form view controllers class extends ZaXFormViewController class. This way, the new class gets access to all default implementations of the common methods of a form view controller.

function ZaSambaDomainController(appCtxt, container, app) {

  ZaXFormViewController.call(

    this, appCtxt, container,app,

    "ZaSambaDomainController");

  this._UICreated = false;

  this._toolbarOperations = new Array();

}

ZaSambaDomainController.prototype = new ZaXFormViewController();

ZaSambaDomainController.prototype.constructor = ZaSambaDomainController;

ZaController.initToolbarMethods["ZaSambaDomainController"] = new Array();

ZaController.setViewMethods["ZaSambaDomainController"] = new Array();

/**

* @method initToolbarMethod

* This method creates ZaOperation objects 

* All the ZaOperation objects are added to this._toolbarOperations array which is then used to 

* create the toolbar for this view.

* Each ZaOperation object defines one toolbar button.

* Help button is always the last button in the toolbar

**/

ZaSambaDomainController.initToolbarMethod = 

function () {

  this._toolbarOperations.push (

    new ZaOperation(

      ZaOperation.SAVE, 

      ZaMsg.TBB_Save,

      ZaMsg.SERTBB_Save_tt,

      "Save", 

      "SaveDis", 

      new AjxListener(this, this.saveButtonListener)

    )

  );

  this._toolbarOperations.push(

    new ZaOperation(

      ZaOperation.CLOSE, 

      ZaMsg.TBB_Close, 

      ZaMsg.SERTBB_Close_tt,

      "Close", 

     "CloseDis", 

     new AjxListener(this, this.closeButtonListener)

   )

 );    

}

ZaController.initToolbarMethods["ZaSambaDomainController"].push(

ZaSambaDomainController.initToolbarMethod);

/**

*@method setViewMethod 

*@param entry - isntance of ZaSambaDomain class

*/

ZaSambaDomainController.setViewMethod =

function(entry) {

  if(entry.name && entry.id)

    entry.load("name", entry.name);

  if(!this ._UICreated) 

    this._createUI();

    this._app.pushView(this.getContentViewId());

  this._contentView.setDirty(false);

/* setObject is delayed to be called after pushView in order to avoid jumping of the view */

  this._contentView.setObject(entry); 

  this._currentObject = entry;

}

ZaController.setViewMethods["ZaSambaDomainController"].push(

ZaSambaDomainController.setViewMethod);

/**

* @method _createUI

**/

ZaSambaDomainController.prototype._createUI =

function () {

  this._contentView = this._view = 

    new ZaSambaDomainXFormView(this._container, this._app);

  this._initToolbar();

  this._toolbar = new ZaToolBar(

    this._container, 

    this._toolbarOperations);

  var elements = new Object();

  elements[ZaAppViewMgr.C_APP_CONTENT] = this._contentView;

  elements[ZaAppViewMgr.C_TOOLBAR_TOP] = this._toolbar;

  var tabParams = {

     openInNewTab: true,

     tabId: this.getContentViewId()

  };  

  this._app.createView(this.getContentViewId(), elements, tabParams);

  this._app._controllers[this.getContentViewId ()] = this;    

  this._UICreated = true;

}

ZaSambaDomainController.prototype._saveChanges =

function () {

  var mods = new Object();

  for (var a in obj.attrs) {

    if(a == ZaItem.A_objectClass )

      continue;

    if(this._currentObject.attrs[a] != obj.attrs[a] ) {

mods[a] = obj.attrs[a];

    }

  }

  try {

    this._currentObject.modify(mods);

    this.fireChangeEvent(this._currentObject);

  } catch (ex) {

    var detailStr = "";

    for (var prop in ex) {

      if(ex[prop] instanceof Function) 

continue;

      detailStr = detailStr + prop + " - " + ex[prop] + "\n";

    }

    this._handleException(ex,

      "ZaSambaDomainController.prototype._saveChanges", 

      null, 

      false);

    return false; 

  }

  this._contentView.setDirty(false);

  return true;

}

Constructing a form view controller object

The constructor is similar to list view controller constructor. Only the first statement in the constructor is required – the call to the parent contructor. The last argument of the parent constructor call is the String representation of the name of this class.

ZaXFormViewController.call(

    this, appCtxt, container,app,

    "ZaSambaDomainController");

Creating contents of the form view

Method ZaSambaDomainController.prototype._createUI instantiates elements of the UI: tool bar and form view. After being instantiated, these elements are assigned to the main application controller which takes care of the layout

this._app.createView(this.getContentViewId(), elements, tabParams); 

Object elements is a map where keys correspond to a predefined position of the element on the screen: ZaAppViewMgr.C_APP_CONTENT for the main content area and ZaAppViewMgr.C_TOOLBAR_TOP for the tool bar area. When we call this._app.createView method, layout manager arranges the elements on the screen according to the application skin.

Registering a form view controller

Just like we had to register the list view controller, we also need to register the form view controller. However, in ZCS 5.0 form view controllers are not singletons. An instance of a controller is created for each open form. When the form is closed, the isntance is destroyed.

In order to register the form view controller we need to add another getter method to ZaApp class.

Code example

     ZaApp.prototype.getSambaDomainController = function() {

       var c = new ZaSambaDomainController(

         this._appCtxt, 

         this._container, 

         this

       );

  var ctrl = this.getSambaDomainListController();

  c.addChangeListener(new AjxListener(ctrl, ctrl.handleChange));

  c.addCreationListener(new AjxListener(ctrl, ctrl.handleCreation));

  c.addRemovalListener(new AjxListener(ctrl, ctrl.handleRemoval)); 

       return c;

}

Showing the form view

In order to show the form view, you need to call show method of the corresponding form view controller. In this example, we do not overwrite this method, because the default implementation is sufficient. Default implementation of show method runs through all functions referenced in ZaController.setViewMethods["ZaSambaDomainController"] array. Therefore, we define ZaSambaDomainController.setViewMethod function and add it to the array.

Defining a tool bar for a form view

ZaController.initToolbarMethods["ZaSambaDomainController"] = new Array();

This statement initializes the array of method references in ZaController.initToolbarMethods map with the key that corresponds to this controller class. The key should be the same value that we passed to the parent constructor call in the constructor.

A tool bar is created by the ZaController.prototype._initToolbar method based on this._toolbarOperations array. First, ZaController.prototype._initToolbar calls functions referenced in ZaController.initToolbarMethods["ZaSambaDomainController"] array; next, it creates tool bar buttons based on the ZaOperation objects found in this._toolbarOperations array. Each ZaOperation object describes one button in a tool bar. In this example, the tool bar will have two buttons: Save and Close.

Code example

this._toolbarOperations.push (

    new ZaOperation(

      ZaOperation.SAVE, 

      ZaMsg.TBB_Save,

      ZaMsg.SERTBB_Save_tt,

      "Save", 

      "SaveDis", 

      new AjxListener(this, this.saveButtonListener)

    )

  );

  this._toolbarOperations.push(

    new ZaOperation(

      ZaOperation.CLOSE, 

      ZaMsg.TBB_Close, 

      ZaMsg.SERTBB_Close_tt,

      "Close", 

     "CloseDis", 

     new AjxListener(this, this.closeButtonListener)

   )

 );


Understanding resource files

Resource file of an extension is a text file with the name “name of the extension.properties”. I.e. for zimbra_samba extension, the resource file is zimbra_samba.properties. The purpose of the resource file is to allow developers to localize extensions. A resource file consists of name value pairs, where a name is a name of a constant, and value is plain text or a pattern. As mentioned earlier, a resource file is translated into JavaScript file by Zimbra server when a browser requests the resource. For example, if your resource file is zimbra_samba.properties, and it contains the following name-value pair

     Domain_name_label = Samba domain name


The generated JavaScript file will be zimbra_samba.js and will contain the following code:

function zimbra_samba () {}

zimbra_samba. Domain_name_label = “Samba domain name”;


Thus, if you add resource files to your extension, instead of using strings for displayed text, you can use constants. In order to avoid null-reference errors, resource file is always loaded before all other JavaScript files of an extension.

Jump to: navigation, search