diff --git a/.gitignore b/.gitignore
index c4dcc104ca873c7fec9f8939e07ecf4e14be73e2..93d0325152423212bdd5503e6bb8fae563f3b012 100755
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ errors/
 private/
 sessions/
 uploads/
+web2py.plugin.dbui.w2p
diff --git a/static/plugin_dbui/dbui-debug.js b/static/plugin_dbui/dbui-debug.js
new file mode 100644
index 0000000000000000000000000000000000000000..69392cfc2cbb4e1bfb78903f98c446fe2c468428
--- /dev/null
+++ b/static/plugin_dbui/dbui-debug.js
@@ -0,0 +1,1892 @@
+/**
+ * appbase.js
+ * 
+ * Constants and elementary functions to built an application
+ * 
+ * $Id$
+ * 
+ */
+Ext.namespace('App');
+
+/**
+ * @cfg {String} App.name
+ * Name of the web application.
+ * This constants is defined by the server.
+ */
+
+/**
+ * @cfg {Array} App.storeCfgs
+ * Configuration option for all json stores
+ */
+
+/**
+ * @param {String} App.version version of the library
+ */
+App.version = '0.4.3';
+
+/**
+ * Helper function mimicking the encode_field function running on the server
+ * (table, field) → TableField
+ * @param {String} table
+ * @param {String} field
+ */
+App.encodeField = function (table, field) {
+    var t, f;
+    t = table[0].toUpperCase() + table.slice(1, table.length);
+    f = field[0].toUpperCase() + field.slice(1, field.length);
+    return t + f;
+};
+
+/**
+ * Helper function returning the App.data.DirectStore identifies by its id.
+ * If the store does not exit it is created and register in the store manager.
+ * 
+ * NOTE: The storeId is defined by the server for each widget. 
+ * The syntax of the storeId is "tableStore". 
+ * If the store does not exist, its configuration is extract from 
+ * the application constant App.storeCfgs.Then it is created and registered.
+ * In the current frame work a unique store is associated to a database table.
+ * This property guaranty that widget share the same store.
+ * 
+ * @param {String} storeId
+ */
+App.getDirectStore = function (storeId) {
+    
+    var cfg,
+        store,
+        table;
+    
+    store = Ext.StoreMgr.lookup(storeId);
+    
+    if (!store && !Ext.isString(storeId)) {
+        throw new Error('Fail to instanciate the store: "' + storeId + '"');
+    }    
+
+    if (!store && Ext.isString(storeId)) {
+
+        table = storeId.slice(0, storeId.search('Store'));
+        cfg = App.storeCfgs[table];
+        store = new App.data.DirectStore(cfg);
+    }
+
+    return store;
+};
+
+/**
+ * Helper function returning the name of the database table 
+ * associated to the store. works with store configured using the
+ * Dbui.getJsonStore method.
+ * @param {App.data.DirectStore} store
+ */
+App.getTableName = function (store) {
+    return store.baseParams.tableName;    
+};
+
+/**
+ * Helper function to determine if a plugin is present 
+ * in the plugins list of a component
+ * @param {Ext.Component} 
+ * @param {String} ptype the ptype of the plugin
+ */
+App.isPlugin = function (component, ptype) {
+
+    var i, 
+        plugin;
+
+    if (!component.hasOwnProperty('plugins')) {
+        return false;
+    }
+
+    for (i = 0; i < component.plugins.length; i += 1) {
+
+        plugin = component.plugins[i];
+
+        if (plugin === ptype) {
+            return true;
+        }
+            
+        if ((typeof (plugin) === 'object') && plugin.hasOwnProperty('ptype') && (plugin.ptype === ptype)) {
+            return true;
+        }   
+    }
+    return false;
+};
+/**
+ *  A border layout with a panel and a selector.
+ *  
+ *  The selector is a collapsible panel appearing on the right side.
+ *  It contains a set of Fields usually organized in field sets.
+ *  By default the selector contains two buttons Go and Reset.
+ *  
+ *  The logic between the panel, the selector and the buttons 
+ *  is defined in inherited class.
+ *  
+ *  The type of this component is xpanelwithselector.
+ *  
+ *  @extend: Ext.Panel
+ *  
+ */
+Ext.namespace('App');
+
+App.BasePanelWithSelector = Ext.extend(Ext.Panel, {
+ 
+    /**
+     * @param {Object} panel and selector configurations
+     */
+    panelCfg: null,
+    selectorCfg: null,
+
+    /**
+     * Private attribute for internationalization
+     */
+    textGo: 'Go',
+    textReset: 'Reset',
+
+    /**
+     * constructor
+     * @param {Object} config
+     */
+    constructor: function(config){
+
+        var cfg,
+            panel,
+            selector;    
+
+        Ext.apply(this, config);
+
+        panel = Ext.ComponentMgr.create(this.panelCfg);
+        selector = Ext.ComponentMgr.create(this.selectorCfg);
+
+        // predefined configuration of the panel
+        cfg = {
+            layout: 'border',
+            items: [{
+                border: false,
+                layout: 'fit',
+                itemId: 'mainPanel',
+                items: [panel],
+                region: 'center',
+            }, {
+            	buttons: [{
+                    ref: '../../goButton', 
+                    text: this.textGo
+                }, {
+                    ref: '../../resetButton', 
+                    text: this.textReset
+                }],      
+                collapsible: true,
+                defaults: {anchor: '99%'},
+                frame: true,
+                layout: 'form',
+                itemId: 'selectorPanel',
+                items: [selector],
+                region: 'east',
+                split: true,
+                width: 300,
+            }]
+        };
+
+        Ext.apply(this, cfg);
+        
+        // instanciate the panel
+        App.BasePanelWithSelector.superclass.constructor.call(this);
+    }
+});
+
+Ext.reg('xpanelwithselector', App.BasePanelWithSelector);/**
+ * A button to download file from the server
+ * 
+ * @extends Ext.Button
+ * @version $Id$
+ *
+ */
+
+Ext.namespace('App');
+
+App.ButtonDownload = Ext.extend(Ext.Button, {
+
+    /**
+     * @cfg{String} url  pointing to the server controller  
+     * which will return the file
+     */    
+    url: undefined,
+    
+     /**
+     * private method requests by the component model of ExtJS
+     */
+    initComponent: function () {
+        
+        // initialize the underlying class
+        App.ButtonDownload.superclass.initComponent.call(this);
+        
+        // download when the button is pressed
+        this.on('click', this.onDownload, this);
+    },
+    
+    /**
+     * Handler to download the file
+     * The scope is the button.
+     * 
+     * NOTE: this method add and iframe at the end of the DOM document
+     * the source of the iframe is the url pointing on the server conroller.
+     * The latter returun a document which is automatically loaded.
+     * 
+     * Method suggested on the sencha forum:
+     * www.sencha.com/forum/showthread.php?26471-Download-File-on%28-click-%29
+     */
+    onDownload: function (button, event) {
+    
+        // remove existing iframe
+        try {
+            Ext.destroy(Ext.get('downloadIframe'));
+        } catch (err) {}
+        
+        // create a fresh iframe
+        Ext.DomHelper.append(document.body, {
+            tag: 'iframe',
+            id: 'downloadIframe',
+            frameBorder: 0,
+            width: 0,
+            height: 0,
+            css: 'display:none;visibility:hidden;height:0px;',
+            src: this.url
+        });
+    }   
+});
+
+Ext.reg('xbuttondownload', App.ButtonDownload);
+/**
+ * The ComboBox is an Ext.form.ComboBox with an App.data.DirectStore. 
+ * 
+ * The type of this component is xcombobox.
+ * 
+ * @extends Ext.form.ComboBox 
+ * @version $Id$
+ *
+ */
+Ext.namespace('App.form');
+
+App.form.ComboBox = Ext.extend(Ext.form.ComboBox, {
+
+    /**
+     * Predefined setting
+     *
+     */
+    mode: 'remote',
+    editable: false,
+    selectOnFocus: true,
+    triggerAction: 'all',
+    typeAhead: true,
+    
+    /**
+     * private method require by the ExtJs component model
+     */	
+    initComponent: function () {
+
+        // defined the empty text from the field label
+        if (this.fieldLabel && !this.emptyText) {
+            this.emptyText = "Select a " + this.fieldLabel + " ...";	
+        }
+
+        // link the combobox to the data store
+        this.store = App.getDirectStore(this.store);
+
+        // construct the underlying class. DON'T MOVE
+        App.form.ComboBox.superclass.initComponent.call(this);
+
+        // load the store if not yet done
+        if (this.store.getCount() === 0) {
+            this.store.load();    
+        }
+                
+        // Set the default value when the comboBox is not yet expend
+        this.store.on('load', function () {
+            this.setValue(this.initialConfig.value);
+        }, this);
+    }
+});
+
+Ext.reg('xcombobox', App.form.ComboBox);
+/**
+ * The FormPanel is an Ext.form.formPanel with an App.data.DirectStore. 
+ * It comes with two button 'Action' and 'Reset' and a set of method to 
+ * create, duplicate, destroy and update record in the direct store.
+ * 
+ * This object can be used as a classic form, a row editor for a grid, 
+ * a record browser, ....
+ * 
+ * The action associated to the button is set via the setAction method.
+ * the create action is set by default.
+ * 
+ * The type of this component is xform.
+ * 
+ * @extends Ext.form.FormPanel
+ * @version $Id$
+ * 
+ */
+Ext.namespace('App.form');
+
+App.form.FormPanel = Ext.extend(Ext.form.FormPanel, {
+
+    /**
+     * @cfg {Ext.data.Store} store the Ext.data.Store that the form should use.
+     * The store has to be defined since the actions rely on it.
+     */
+    store: null,
+    
+    /**
+     * Predefined configuration options
+     */
+    autoScroll: true,
+    bodyStyle: 'padding:5px 5px 0',
+    buttons: [{
+        formBind: true,
+        text: 'Action',
+        ref: '../buttonAction'
+    }, {
+        text: 'Reset',
+        ref: '../buttonReset'
+    }],
+    defaults: {anchor: '100%'},
+    defaultType: 'textfield',
+    frame: true,
+    monitorValid: true,
+    
+    /**
+     * private attribute to keep track of current action parameters
+     */
+    currentAction: null,
+    currentRecord: null,
+    
+    /**
+     * private attributes for internationalization
+     */
+    textCreate: 'Create',
+    textDestroy: 'Delete',
+    textDuplicate: 'Duplicate',
+    textReset: 'Reset',
+    textUpdate: 'Update',
+    
+    /**
+     * private method require by the ExtJs component model
+     */	
+    initComponent: function () {
+        
+        // helper function to reset the form
+        // the scope is Ext.data.BasicStore
+        function resetForm() {
+            this.reset();
+        }
+        
+        // construct the underlying class. DON'T MOVE
+        App.form.FormPanel.superclass.initComponent.call(this);
+        
+        // button events listeners
+        this.buttonAction.on('click', this.doAction, this);
+        this.buttonReset.on('click', resetForm, this.getForm());
+        
+        // activate fields tooltip listeners
+        this.addFieldToolTipsListeners();
+        
+        // link the form to the data store
+        this.store = App.getDirectStore(this.store);
+        this.store.on('exception', this.onStoreException, this);
+        this.store.on('write', resetForm, this.getForm());    
+
+        // set the default action: create
+        this.setAction('create');
+        this.buttonReset.setText(this.textReset);
+    },
+
+    /**
+     * Private method to register fields tooltip
+     * Require the configuration parameter Ext.form.Field.tipText.
+     */
+    addFieldToolTipsListeners: function () {
+        var form;
+
+        function createToolTip(c) {
+            //NOTE: don't remove the new keyword although JSlint require it
+            new Ext.ToolTip({
+                target: c.getEl(),
+                title: c.fieldLabel,
+                anchor: 'left',
+                trackMouse: false,
+                html: Ext.util.Format.htmlEncode(c.tipText)
+            }); 
+        }   
+             
+        form = this.getForm();
+        form.items.each(function (field) {
+            if (field.tipText) {
+                field.on('render', createToolTip);
+            }
+        });
+    },
+
+    /**
+     * Private method to enable/disable all fields of the form
+     * @param {Object} boolean
+     */
+    disableFields: function (bool) {
+        
+        var form = this.getForm();
+        
+        form.items.each(function (field) {
+            field.setDisabled(bool);
+        });
+
+    },
+
+    /**
+     * Handler to perform the current action on the store.
+     * The current action is set by the setAction method.
+     * Should not be call directly, except for delete without confirmation.
+     * Raise an error if the store is not defined.
+     */
+    doAction: function () {
+
+        var form = this.getForm(),
+            newRecord;
+        
+        if (!form.isValid()) {
+            return;
+        }
+        
+        if (!this.store) {
+            throw new Error('the store is undefined !!!');
+        }
+        
+        switch (this.currentAction) {
+
+        case 'create':
+            newRecord = new this.store.recordType();
+            this.updateRecord(newRecord);
+            this.store.add(newRecord);
+            break;  
+            
+        case 'destroy':
+            this.store.remove(this.currentRecord);
+            break;
+            
+        case 'duplicate':
+            newRecord = new this.store.recordType();
+            this.updateRecord(newRecord);
+            this.store.add(newRecord);
+            break;
+            
+        case 'update':
+            this.currentRecord.beginEdit();
+            this.updateRecord(this.currentRecord);
+            this.currentRecord.endEdit();
+            break;
+        }
+        if (this.store.autoSave === false) {
+            this.store.save();    
+        }
+    },
+    
+    /**
+     * Private method to hardreset the form.
+     * 
+     * The reset method resets the field value to the originally loaded.
+     * 
+     * The hardreset erases the originally loaded values and reset the form.
+     * Hardreset is required to clean form in sequence like update, create,...
+     * It handle properly default value for combobox.
+     * 
+     */
+    hardReset: function () {
+        var form = this.getForm();
+        
+        form.items.each(function (field) {
+            field.originalValue = Ext.value(field.initialConfig.value, '');
+            field.setValue(field.originalValue);
+        });
+    },
+    
+    /**
+     * Handler to process store exception
+     * the scope of this function is App.form.FormPanel
+     * @param {Object} proxy
+     * @param {Object} type
+     * @param {Object} action
+     * @param {Object} options
+     * @param {Object} response
+     * @param {Object} arg
+     */
+    onStoreException: function (proxy, type, action, options, response, arg) {
+
+        var field,
+            fieldName,
+            form = this.getForm(),
+            record;
+
+        // mark fields invalid
+        for (fieldName in response.errors) {
+            if (response.errors.hasOwnProperty(fieldName)) {
+                field = form.findField(fieldName);
+                field.markInvalid(response.errors[fieldName]);           
+            }
+        }
+        
+        // remove the bad record from the store
+        // otherwise it will be send again in the next transaction
+        record = arg[0];
+        this.store.remove(record);
+    },
+    
+    /**
+     * Public method to set the action associated to the form. 
+     * The record is load into the form and the text of the button is 
+     * properly set. Understood action are: create, destroy, duplicate, 
+     * update and view.
+     * @param {Object} action
+     * @param {Object} record
+     */
+    setAction: function (action, record) {
+
+        var form = this.getForm(),
+            table,
+            tableId;
+        
+        this.buttonReset.show();
+        this.buttonAction.show();
+        
+        this.disableFields(false);
+        
+        this.currentAction = action;
+        this.currentRecord = record;
+        
+        switch (action) {
+
+        case 'create':
+            this.hardReset();
+            this.buttonAction.setText(this.textCreate);
+            break;  
+            
+        case 'destroy':
+            this.buttonAction.setText(this.textDestroy);
+            form.loadRecord(record);
+            break;
+            
+        case 'duplicate':
+            this.buttonAction.setText(this.textDuplicate);
+            this.hardReset();
+            table = App.getTableName(this.store);
+            tableId = App.encodeField(table, 'id');
+            delete record.data[tableId];
+            form.loadRecord(record);
+            break;
+            
+        case 'update':
+            this.buttonAction.setText(this.textUpdate);
+            form.loadRecord(record);
+            break;
+            
+        case 'view':
+            this.buttonReset.hide();
+            this.buttonAction.hide();
+            form.loadRecord(record);
+            this.disableFields(true);
+            break;
+        }
+    },
+    
+    /**
+     * Private method to update the selected record with the value of the form
+     * This method have been designed to handle foreign keys, date object, ....
+     * @param {Object} record
+     */
+    updateRecord: function (record) {
+
+        var combo,
+            field,
+            fields = [], 
+            i,
+            items,
+            rec,
+            value;
+
+        // get the list of dirty fields 
+        items = this.findByType('field');
+        for (i = 0; i < items.length; i += 1) {
+            field = items[i];
+            if (field.getXType() !== 'compositefield') {
+                fields.push(field);
+            }
+        }
+
+        // include dirty fields embedded in composite fields 
+        items = this.findByType('compositefield');
+        for (i = 0; i < items.length; i += 1) {
+            items[i].items.eachKey(function(key, subfield) {
+                fields.push(subfield);
+            });
+        }
+
+        // update the record
+        // take care of special treatment required by date and combobox
+        for (i = 0; i < fields.length; i += 1) {
+            
+            field = fields[i];
+            value = field.getValue();
+
+            switch (field.getXType()) {
+                
+                // We convert the date object according to a string defined
+                // by the Ext.form.DateField property format.
+                // NOTE: by default in the json encoding, the date object
+                // is converted as string using iso format YYYY-MM-DDTHH:MM:SS.
+                // However, the string expected by the database depends on 
+                // the date type: date, datetime or time. The format of the
+                // Ext.form.DateField, used by the interface, is defined in the
+                // property format.
+
+                case 'datefield':
+                    if (Ext.isDate(value)) {
+                        value = value.format(field.format);
+                    }
+                    break;
+
+                // For foreign key, the record contains the valueField 
+                // as well as the displayField. The default action update
+                // the valueField but note the display field. 
+                // The next lines append the displayField
+
+                case 'xcombobox':
+                    combo = field;
+                    rec = combo.findRecord(combo.valueField, combo.getValue());
+                    record.set(combo.displayField, rec.get(combo.displayField));            
+                    break;
+            }
+
+            record.set(field.getName(), value);
+        }
+               
+    }
+
+});
+
+Ext.reg('xform', App.form.FormPanel);
+/**
+ * The GridFilter class
+ * 
+ * Associate to grid, it contains all the logic to filter the content of a grid
+ * according to the value define in the form.
+ * 
+ * @version $Id$
+ *
+ */
+
+Ext.namespace('App.grid');
+
+App.grid.GridFilter = Ext.extend(Ext.form.FieldSet, {
+
+    /**
+     * Private
+     * Dictionary with additional where clause
+     */	
+    filterConditions: {},
+    
+    /**
+     * Private
+     * Initial where clause
+     */
+    initialFilterConditions: [],
+    
+    /**
+     * Private
+     * Reference to the paging toolBar
+     */
+    pagingToolbar: null,
+
+    /**
+     * Private 
+     * a reference to the grid store
+     */
+    store: null,
+    
+    /**
+     * Predefined setting
+     * 
+     * NOTE: 
+     * the enableKeyEvents is mandatory for Ext.form.TextField
+     */
+    defaults: {
+        anchor: '99%',
+        enableKeyEvents: true
+    },
+    
+    /**
+     * private method requests by the component model of Ext JS
+     */
+    initComponent: function () {
+
+        var field, 
+            fields, 
+            i;
+        
+        // initialize the underlying class.
+        App.grid.GridFilter.superclass.initComponent.call(this);
+                        
+        // associate handlers to fields
+        fields = this.findByType('field');
+        for (i = 0; i < fields.length; i += 1) {
+            field = fields[i];
+            
+            // catch a change of value for comboBox
+            if (field.xtype === 'xcombobox') {
+                field.on('select', this.onChange, this);
+                
+            } else {
+                // catch the key pressed enter for other fields
+                // wait that user finish to type to run the handler
+                field.on('keyup', this.onChange, this, {buffer: 500});
+            }
+        }
+    },
+    
+    /**
+     * Bind the grid filter to the grid
+     * @param {Ext.grid.GridPanel} grid
+     */
+    bind: function (grid) {
+
+        var baseParams, 
+            i;
+        
+        this.store = grid.getStore();
+        
+        // paging toolbar ?
+        if (grid.pagingInitialized) {
+            this.pagingToolbar = grid.getBottomToolbar();
+        }
+        
+        // initial filter conditions
+        baseParams = this.store.baseParams;
+        for (i = 0; i < baseParams.where.length; i += 1) {
+            this.initialFilterConditions.push(baseParams.where[i]);
+        }
+                
+    },
+
+    /**
+     * Handler to catch a change in field value
+     */
+    onChange: function (field) {
+        this.setupCondition(field);
+    },
+
+    /**
+     * Handler to reset the filter
+     */
+    onReset: function () {
+        
+        var fields, 
+            i;
+        
+        // reset the field value
+        fields = this.findByType('field');
+        for (i = 0; i < fields.length; i += 1) {
+            fields[i].reset();
+        }
+        
+        // reset filter conditions
+        this.filterConditions = {};
+        this.store.baseParams.where = this.initialFilterConditions;
+        
+        // update the store
+        this.updateStore();
+    },
+    
+    /**
+     * Setup condition
+     * @param {Ext.form.Field}
+     */
+    setupCondition: function (field) {
+
+        var conditions = [], 
+            i, 
+            k, 
+            value, 
+            where;
+        
+        // get the field value and update the condition dictionary
+        value = field.getValue();
+        where = field.name + " " + field.filterOperator + " '" + value + "'";
+        
+        if (value === '') {
+            delete this.filterConditions[field.name];
+        } else {
+            this.filterConditions[field.name] = where;
+        }
+                    
+        // build the complete where clause
+        for (i = 0; i < this.initialFilterConditions.length; i += 1) {
+            conditions.push(this.initialFilterConditions[i]);
+        }
+        
+        for (k in this.filterConditions) {
+            if (this.filterConditions.hasOwnProperty(k)) {
+                conditions.push(this.filterConditions[k]);
+            }
+        }
+        
+        // push new condition in the where clause of the store
+        this.store.baseParams.where = conditions;
+        
+        // update the store
+        this.updateStore();
+    },
+    
+    /**
+     * private
+     * Helper function to load the store applying filter conditions
+     */
+    updateStore: function () {
+        
+        if (this.pagingToolbar) {
+            this.pagingToolbar.doRefresh();
+        } else {
+            this.store.load();
+        }		
+    }
+});
+
+Ext.reg('xgridfilter', App.grid.GridFilter);/**
+ * The Grid class is Ext.grid.GridPanel with a Ext.PagingToolbar
+ * Many functionality can be added through grid plugins like
+ * App.grid.RowEditor, ....
+ * 
+ * The type of this component is xgrid.
+ * 
+ * @extends Ext.gridPanel
+ * @version $Id$
+ * 
+ */
+
+Ext.namespace('App.grid');
+
+App.grid.Grid = Ext.extend(Ext.grid.GridPanel, {
+
+    /**
+     * private attributes used by plugins
+     */
+    rowEditor: null,   
+
+    /**
+     * private method requests by the component model of ExtJS
+     */
+    initComponent: function () {
+
+        // link the grid to the data store
+        this.store = App.getDirectStore(this.store);
+
+        // empty Paging toolBar requires by the plugins
+        this.bbar = new Ext.PagingToolbar();
+        this.bbar.hide();
+
+        // initialize the underlying class. DON'T MOVE
+        App.grid.Grid.superclass.initComponent.call(this);        
+
+        // load the store if not yet done
+        // if the grid paging is used load only the first 10 rows
+        if (!this.store.getTotalCount()) {
+            if (App.isPlugin(this, 'pGridPaging')) {
+                this.store.load({params: {start: 0, limit: 10}});
+            } else {
+                this.store.load();    
+            }
+        }
+    }
+});
+
+Ext.reg('xgrid', App.grid.Grid);/**
+ * Plugin to render mathematics formula embedded in grid content.
+ * The processing is performed by MathJax.
+ * The MathJax library is loaded by the framework
+ * 
+ * @version $Id$
+ *
+ */
+
+Ext.namespace('App.grid');
+ 
+App.grid.MathJax = Ext.extend(Object, {
+
+    init: function (grid) {
+
+        function processMath(gridView) {
+            var domEl = Ext.getDom(gridView.el);
+            MathJax.Hub.Queue(["Typeset",MathJax.Hub, domEl]);
+        }
+        
+        grid.getView().on('refresh', processMath, grid);
+        grid.store.on('write', processMath, grid);
+    }
+});
+    
+Ext.preg('pGridMathJax', App.grid.MathJax);/**
+ * The paging bottom bar plugin
+ * The ptype of this component is pGridPaging.
+ *  
+ *  
+ * NOTE: the number of row load in the grid at the first time
+ * is defined by the grid when loading the store.
+ * 
+ * @version $Id$
+ * 
+ */
+Ext.namespace('App.grid');
+
+App.grid.Paging = Ext.extend(Object, {
+
+    /**
+     * Private parameter identifying the type of plugin
+     */
+    ptype: 'pGridPaging',
+
+    /**
+     * Private attributs for internationalization
+     */
+    textExport: 'Export',
+    textSlider: 'Rows per page',
+    
+    /**
+     * Plugin initialization
+     */
+    init: function (grid) {
+
+        var bbar;
+        
+        // link the bbar and the grid store
+        bbar = grid.getBottomToolbar();
+        bbar.bindStore(grid.store);
+
+        // add the slider and the export button
+        bbar.add('-', 
+                 this.textSlider, 
+                 {
+                    xtype: 'slider',
+                    plugins: new Ext.slider.Tip(),
+                    listeners: {
+                        changecomplete: this.onChangePageSize,
+                        scope: bbar
+                    },
+                    minValue: 1,
+                    width: 100
+                 }, 
+                 '->',
+                 {
+                    xtype: 'xbuttondownload',
+                    text: this.textExport,
+                    url: App.csvUrl + '?tableName=' + App.getTableName(grid.store)
+                 }
+        );
+        
+        bbar.show();
+        
+        // Initialize the parameters of the paging and slider widgets.
+        // Depends on the status of the store.
+        // Initialization is only performed once.
+        if (grid.store.getTotalCount() > 0) {
+            
+            this.onInit.call(grid, grid.store);
+        
+        } else {
+        
+            grid.store.on('load', this.onInit, grid, {single: true});
+        
+        }
+        
+        // update the slider parameter after a write action
+        // which destroy and create records
+        grid.store.on('write', this.onWrite, grid);
+    },
+    
+    /**
+     * Handler to change the number of rows per page
+     * the scope is the bottom tool bar
+     */
+    onChangePageSize: function (slider, newValue, thumb) {
+        var bbar = this;
+        bbar.pageSize = newValue;
+        bbar.moveFirst();
+    },
+    
+    /**
+     * Handler to initialize the number of rows per page and the number of page
+     * the scope is the grid
+     */
+    onInit: function (store, records, options) {
+
+        var bbar, 
+            grid = this, 
+            nRows = store.getCount(), 
+            slider;
+                
+        bbar = grid.getBottomToolbar();
+        bbar.pageSize = nRows;
+
+        slider = bbar.findByType('slider')[0];
+        slider.setMaxValue(store.getTotalCount());
+        slider.setValue(nRows);
+        
+    },
+    
+    /**
+     * Handler to update the slider/bottomToolBar parameters after a store write action.
+     * The latter create or destroy records in the store.
+     * The scope is the bottom grid
+     */
+    onWrite: function () {
+        
+        var bbar,
+            grid = this,
+            slider;
+
+        bbar = grid.getBottomToolbar();
+        bbar.pageSize = grid.store.getCount();
+        
+        slider = bbar.findByType('slider')[0];
+        slider.setMaxValue(grid.store.getTotalCount());
+        slider.setValue(grid.store.getCount());
+        
+    }
+});
+
+Ext.preg('pGridPaging', App.grid.Paging);
+/**
+ * The row editor context menu plugin
+ *
+ * Display the context menu when the user right click on a row.
+ * The content of the menu allows to manipulate the App.grid.RowEditor
+ * 
+ * Requires the App.grid.GridRowEditor plugin.
+ * 
+ * The ptype of this component is pGridRowEditorContextMenu.
+ * 
+ * @extends Object
+ * @version $Id$
+ * 
+ */
+
+Ext.namespace('App.grid');
+
+App.grid.RowEditorContextMenu = Ext.extend(Object, {
+
+    /**
+     * Private attribute identifying the type of plugin
+     */
+    ptype: 'pGridRowEditorContextMenu',
+
+    /**
+     * private attributes for internationalization
+     */
+    textAdd: 'Add',
+    textCreate: 'Create',
+    textDestroy: 'Delete',
+    textDuplicate: 'Duplicate',
+    textUpdate: 'Update',
+    textView: 'View',
+        
+    /**
+     * Plugin initialization
+     * @param {Object} grid
+     */
+    init: function (grid) {
+                
+        var menu = new Ext.menu.Menu();
+        
+        // protection		
+        if (!grid.rowEditor) {
+            throw new Error('no grid row editor !!!');
+        }
+
+        grid.addListener('containercontextmenu', this.onContainerContextMenu, menu);
+        grid.addListener('headercontextmenu', this.onHeaderContextMenu, menu);
+        grid.addListener('rowcontextmenu', this.onRowContextMenu, menu);
+        
+        menu.add({
+            text: this.textAdd,
+            iconCls: 'xaction-create',
+            handler: grid.rowEditor.onAddRow,
+            scope: grid.rowEditor
+        }, '-', {
+            text: this.textDuplicate,
+            iconCls: 'xaction-duplicate',
+            handler: grid.rowEditor.onDuplicateRow,
+            scope: grid.rowEditor
+        }, {
+            text: this.textUpdate,
+            iconCls: 'xaction-update',
+            handler: grid.rowEditor.onEditRow,
+            scope: grid.rowEditor
+        }, {
+            text: this.textView,
+            iconCls: 'xaction-view',
+            handler: grid.rowEditor.onViewRow,
+            scope: grid.rowEditor
+        }, '-', {
+            text: this.textDestroy,
+            iconCls: 'xaction-destroy',
+            handler: grid.rowEditor.onDeleteRow,
+            scope: grid.rowEditor
+        });
+    },
+    
+    /**
+     * Prvate handler displaying the row context menu in an empty grid
+     */
+    onContainerContextMenu: function (grid, event) {
+        var menu = this;
+        
+        event.stopEvent();
+        menu.showAt(event.getXY());
+        
+    },
+    /**
+     * Private handler displaying the context menu in the header
+     * Nothing is display in that case.
+     */
+    onHeaderContextMenu: function (grid, columIndex, event) {
+        event.stopEvent();
+    },
+    /**
+     * Private handler displaying the row context menu
+     * The scope use by the handler is the menu object.
+     */
+    onRowContextMenu: function (grid, rowIndex, event) {
+        
+        var menu = this;
+        
+        event.stopEvent();
+        grid.selModel.selectRow(rowIndex);		
+        menu.showAt(event.getXY());
+
+    }
+
+});
+
+Ext.preg('pGridRowEditorContextMenu', App.grid.RowEditorContextMenu);
+/**
+ * The grid row editor plugin
+ * The gridRowEditor is a window containing a formPanel.
+ * It exposes series of handlers to delete, duplicate, update, view the 
+ * selected row of the grid, as well as to create new row.
+ *
+ * This plugin limit the number of selected row to one.
+ * 
+ * The ptype of this component is pgridroweditor.
+ * 
+ * @extends Ext.Window
+ * @version $Id$
+ * 
+ */
+
+Ext.namespace('App.grid');
+
+App.grid.RowEditor = Ext.extend(Ext.Window, {
+
+    /**
+     * Private attribute identifying the type of plugin
+     */
+    ptype: 'pGridRowEditor',
+    formPanel: null,
+    grid: null,
+
+    /**
+     * Private attributes for internationalization
+     */
+    addTitle: "Create a new record...",
+    deleteTitle: "Delete the record...",
+    duplicateTitle: "Duplicate the record...",
+    editTitle: "Update the record...",
+    viewTitle: "View the record...",
+    textMsg: 'Select a row please',
+    
+    /**
+     * pre-defined configuration options for the window
+     */
+    autoScroll: true,
+    closeAction: 'hide',
+    constrainHeader: true,
+    defaults: {autoScroll: true},
+    modal: true,
+    plain: true,
+
+    /**
+     * Plugin initialization
+     *
+     * @param {Object} grid GridPanel   the grid instantiating the plugin
+     */
+    init: function (grid) {
+
+        var table;
+        
+        this.grid = grid;
+
+        // The grid keeps track of the editor
+        // since it can be shared between different plugins
+        this.grid.rowEditor = this;
+        
+        // The user can only select one row of the grid
+        this.grid.selModel = new Ext.grid.RowSelectionModel({
+            singleSelect: true
+        });
+                
+        // Add a listener to hide the window when the transaction is successful
+        this.grid.store.on('write', this.onWrite, this);
+
+        // get the formPanel configuration and instantiate the object
+        table = App.getTableName(this.grid.store);
+        Dbui.getForm(table, this.addFormPanel, this);
+    },
+
+    /**
+     * Private call-back method to instantiate the formPanel
+     * the scope is the gridRowEditor
+     * @param {Object} provider
+     * @param {Object} response
+     */
+    addFormPanel: function (provider, response) {
+        var formCfg = response.result;
+        
+        // instantiate the form
+        this.formPanel = new App.form.FormPanel(formCfg);
+        this.add(this.formPanel);
+
+        // add a listeners to catch the store exception
+        this.grid.store.on('exception', 
+                           this.formPanel.onStoreException, 
+                           this.formPanel);
+    },
+    /**
+     * Private method to return the record
+     * associate to the row selected in the grid
+     *
+     * In absence of selected row a warning message is shown
+     * and the function return false.
+     */
+    getSelected: function () {
+
+        var record = this.grid.getSelectionModel().getSelected();
+
+        if (!record) {
+            Ext.MessageBox.alert('Warning', this.textMsg);
+            return false;
+        }
+
+        return record;
+    },
+    
+    /**
+     * Handler to add a new row.
+     * The scope use by the handler is the plugin object.
+     *
+     */
+    onAddRow: function () {
+
+        this.formPanel.setAction('create');
+        this.setTitle(this.addTitle);
+        this.show();
+    },
+
+    /**
+     * Handler to delete the selected row without confirmation
+     * The scope use by the handler is the plugin object.
+     *
+     */
+    onDeleteRow: function () {
+
+        var record = this.getSelected();
+        if (!record) {return; }
+
+        this.formPanel.setAction('destroy', record);
+        this.setTitle(this.deleteTitle);
+        this.formPanel.doAction();
+    },
+
+    /**
+     * Handler to duplicate the selected row
+     * The scope use by the handler is plugin object.
+     *
+     */
+    onDuplicateRow: function () {
+
+        var record = this.getSelected();
+        if (!record) {return; }
+
+        this.formPanel.setAction('duplicate', record);
+        this.setTitle(this.duplicateTitle);
+        this.show();
+    },
+
+    /**
+     * Handler to edit the selected row.
+     * The scope use by the handler is the plugin object.
+     *
+     */
+    onEditRow: function () {
+
+        var record = this.getSelected();
+        if (!record) {return; }
+
+        this.formPanel.setAction('update', record);
+        this.setTitle(this.editTitle);
+        this.show();
+    },
+
+    /**
+     * Handler to view the selected row in the form.
+     * The scope use by the handler is the plugin object.
+     *
+     */
+    onViewRow: function () {
+
+        var record = this.getSelected();
+        if (!record) {return; }
+
+        this.formPanel.setAction('view', record);
+        this.setTitle(this.viewTitle);
+        this.show();        
+    },
+
+    /**
+     * Private handler call when the write transaction is successful.
+     * It aim is to hide the window.
+     * @param {Object} store
+     * @param {Object} action
+     * @param {Object} result
+     * @param {Object} transaction
+     * @param {Object} record
+     */
+    onWrite: function (store, action, result, transaction, record) {
+        this.hide();
+    }    
+});
+
+Ext.preg('pGridRowEditor', App.grid.RowEditor);/**
+ * Add a toolbar with buttons to manipulate the App.grid.RowEditor.
+ * Requires the App.grid.GridRowEditor plugin.
+ * 
+ * The ptype of this component is pGridRowEditorToolbar.
+ *  
+ * @version $Id$
+ * 
+ */
+Ext.namespace('App.grid');
+
+App.grid.RowEditorToolbar = Ext.extend(Object, {
+
+    /**
+     * Private parameter identifying the type of plugin
+     */
+    ptype: 'pGridRowEditorToolbar',
+    
+    /**
+     * Plugin intiaisation
+     * @param {Object} grid
+     */
+    init: function (grid) {
+
+        var toolbar;
+        
+        // protection       
+        if (!grid.rowEditor) {
+            throw new Error('no grid row editor !!!');
+        }
+        
+        toolbar = grid.getTopToolbar();
+        
+        toolbar.add([{
+            text: 'Add',
+            iconCls: 'silk-add',
+            handler: grid.rowEditor.onAddRow,
+            scope: grid.rowEditor
+        }, ' ', {
+            text: 'Delete',
+            iconCls: 'silk-delete',
+            handler: grid.rowEditor.onDeleteRow,
+            scope: grid.rowEditor
+        }, ' ', {
+            text: 'Duplicate',
+            iconCls: 'silk-clone',
+            handler: grid.rowEditor.onDuplicateRow,
+            scope: grid.rowEditor
+        }, ' ', {
+            text: 'Update',
+            iconCls: 'silk-update',
+            handler: grid.rowEditor.onEditRow,
+            scope: grid.rowEditor
+        }, ' ', {
+            text: 'View',
+            iconCls: 'silk-view',
+            handler: grid.rowEditor.onViewRow,
+            scope: grid.rowEditor
+        }, '-']);
+    }
+});
+
+Ext.preg('pGridRowEditorToolbar', App.grid.RowEditorToolbar);/**
+ *  A border layout with a grid and its filter
+ *  The type of this component is xgridwithfilter.
+ *  
+ *  @extend: Ext.Panel
+ *  
+ */
+Ext.namespace('App.grid');
+
+App.grid.GridWithFilter = Ext.extend(App.BasePanelWithSelector, {
+
+    initComponent: function() {
+
+        var filter,
+            grid;
+
+        // collpase the grid filter panel
+        this.items[1].collapsed = true;
+        
+        App.grid.GridWithFilter.superclass.initComponent.call(this);
+
+        // bind the filter to the grid
+        filter = this.findByType('xgridfilter')[0];
+        grid = this.findByType('xgrid')[0];
+        filter.bind(grid);
+
+        // connect buttons
+        this.goButton.hide();
+        this.resetButton.on('click', filter.onReset, filter);
+    }
+});
+
+Ext.reg('xgridwithfilter', App.grid.GridWithFilter);
+/**
+ * A JsonStore to read/write in the database using the EXt.Direct protocol.
+ * Design to work with the class DbSvc on the server side.
+ * 
+ * This class is an Ext.dat.DirectStore with an Ext.data.JsonWriter.
+ * 
+ * Defaults configuration parameters of the writer are defined in the 
+ * constructor. Changing them might break the decoding running on the 
+ * server-side and break the behavior of the grid widget.
+ * 
+ * The default API associated to this store is defined in the constructor
+ * 
+ * The type of this component is xdirectstore.
+ * 
+ * @extends Ext.data.DirectStore
+ * @version $Id$
+ * *
+ */
+Ext.namespace('App.data');
+
+App.data.DirectStore = Ext.extend(Ext.data.DirectStore, {
+        
+    /**
+     * @cfg(Boolean) encode JsonWiter property, by default false
+     * Changing this option might break the decoding running on the server-side
+     * NOTE: the default value is defined in the constructor
+     */
+    /**
+     * @cfg(Boolean) listful JsonWiter property, by default true
+     * Changing this option might break the decoding running on the server-side
+     * NOTE: the default value is defined in the constructor
+     */
+    /**
+     * @cfg(Boolean) writeAllFields JsonWiter property, by default false
+     * Changing this option might break the decoding running on the server-side
+     * NOTE: the default value is defined in the constructor
+     */
+    /**
+     * pre-defined configuration option
+     * The autoSave parameter is helpful for gridRowEditor.
+     */
+    autoLoad: true,
+    autoSave: true,
+    
+    /**
+     * constructor
+     */
+    constructor: function (config) {
+        
+        var cfg, 
+            cfgWrt;
+
+        // configuration option with default value for 
+        // the Ext.data.JsonWriter. Changing these options might break 
+        // the decoding running on the server side.
+        cfg = Ext.apply({}, {
+            encode: false,
+            listful: true,
+            writeAllFields: false
+        }, config);
+
+        // define the remote procedure for store action
+        cfg = Ext.apply(cfg, {
+            api: {
+                create: Dbui.create,
+                destroy: Dbui.destroy,
+                read: Dbui.read,
+                update: Dbui.update
+            }
+        });
+
+        // setup the writer
+        if (!Ext.isDefined(cfg.writer)) {
+            cfgWrt = Ext.copyTo({}, cfg, 'encode, listful, writeAllFields');
+            cfg.writer = new Ext.data.JsonWriter(cfgWrt);    
+        }
+        
+        // instantiate the store
+        App.data.DirectStore.superclass.constructor.call(this, cfg);
+
+        // update the total number of records        
+        this.on('write', this.onWrite);
+    },
+    
+    /**
+     * Handler to update the total number of records after a write action
+     * @param {Object} store
+     * @param {Object} action
+     */
+    onWrite: function (store, action) {
+        
+        switch (action) {
+        case 'create':
+            store.totalLength += 1;
+            break;
+    
+        case 'destroy':
+            store.totalLength -= 1;
+            break;
+        }
+    }
+});
+
+Ext.reg('xdirectstore', App.data.DirectStore);/**
+ * Plugin to render mathematics formula embedded in HTML content.
+ * The processing is performed by MathJax.
+ * The MathJax library is loaded by the framework
+ * 
+ * @version $Id$
+ *
+ */
+
+Ext.namespace('App.panel');
+ 
+App.panel.MathJax = Ext.extend(Object, {
+
+    init: function (panel) {
+
+        // register an handler which is activated only once
+        // when the panel is rendered for the first time
+        panel.on('render', this.onPanelRender, this, {single: true});
+    },
+
+    /**
+     * Handler to activates the MathJax processing
+     * when the html content of the panel is loaded.
+     * 
+     * @param {Ext.Panel} panel
+     */	
+    onPanelRender: function (panel) {
+        var updater = panel.body.getUpdater();
+        updater.on('update', this.onProcess);
+    },
+
+    /**
+     * Handler to run the mathJax processing
+     * @param {Ext.Element} el 
+     * @param {Object} o the response object	
+     */
+    onProcess: function (e, o) {
+        MathJax.Hub.PreProcess();
+        MathJax.Hub.Process();
+    }
+});
+ 
+Ext.preg('pPanelMathJax', App.panel.MathJax);/**
+ * A panel displaying an url content with an collpasible selector.
+ * the selector allow to setup the url parameters and to launch the
+ * request to the server.
+ * 
+ *  The type of this component is xpanelwithurlselector.
+ *  
+ * @extend: App.PanelWithSelector
+ * 
+ */
+Ext.namespace('App');
+
+App.PanelWithUrlSelector = Ext.extend(App.BasePanelWithSelector, {
+    
+    /**
+     * @param {String}
+     */
+    baseUrl: null,
+    
+    /**
+     * Require by the ExtJS model
+     */    
+    initComponent: function () {
+        
+        App.PanelWithUrlSelector.superclass.initComponent.call(this);
+        
+        // connect the buttons
+        this.goButton.on('click', this.onGo, this);
+        this.resetButton.on('click', this.onReset, this);
+    },
+
+    /**
+     * handle to build the URL and to load it in the panel
+     */
+    onGo: function() {
+        
+        var fields,
+            i,
+            panel = this.getComponent('mainPanel'),
+            params = {},
+            selector = this.getComponent('selectorPanel');
+            
+        fields = selector.findByType('field');
+        for (i = 0; i < fields.length; i += 1) {
+            params[fields[i].getName()] = fields[i].getValue();
+        }
+
+        panel.load({
+            url: this.baseUrl,
+            params: params,
+            text: 'Loading...',
+            timeout: 30 
+        });        
+    },
+
+    /**
+     * Handler to reset the selector
+     */
+    onReset: function () {
+        
+        var fields, 
+            i,
+            selector = this.getComponent('selectorPanel');
+
+        
+        fields = selector.findByType('field');
+        for (i = 0; i < fields.length; i += 1) {
+            fields[i].reset();
+        }
+    }
+});
+
+Ext.reg('xpanelwithurlselector', App.PanelWithUrlSelector);
+/**
+ * A pre-configured combobox displaying a set of predefined value.
+ * 
+ * MANDATORY PROPERTIES:
+ *      - model
+ * 
+ * All configuration properties of Ext.form.ComboBox are understood.
+ * 
+ * @extends Ext.form.ComboBox 
+ * @author $Author$
+ * @version $Id$
+ *
+ */
+Ext.namespace('App.form');
+
+App.form.SetBox = Ext.extend(Ext.form.ComboBox, {
+
+    /**
+     * @cfg {Object} model Object to configure the widget. 
+     * It is provided by the server and contains the following keys:
+     * { setData: [...]} 
+     */
+    model: null,
+
+    /**
+     * Predefined setting
+     *
+     */
+    editable: false,
+    emptyText: "Select...",
+    selectOnFocus: true,
+    triggerAction: 'all',
+    typeAhead: true,
+    
+    /**
+     * private method require by the ExtJS component model
+     */	
+    initComponent: function () {
+
+        var i, 
+            li = [];
+        
+        if (!this.model) {
+            throw new Error("the property model is missing !!!");
+        }
+        
+        // setup the store
+        li = [];
+        for (i = 0; i < this.model.setData.length; i += 1) {
+            li.push([this.model.setData[i]]);
+        }		
+
+        this.store = new Ext.data.ArrayStore({
+            fields: [this.model.name],
+            data : li
+        });
+
+        // display field an mode
+        this.displayField = this.model.name;
+        this.mode = 'local';
+        
+        // construct the underlying class. DON'T MOVE
+        App.form.SetBox.superclass.initComponent.call(this);
+
+    }
+});
+
+Ext.reg('xsetbox', App.form.SetBox);/**
+ * The Viewport for the application.
+ * 
+ * The type of this component is xviewport.
+ * 
+ * @extend Ext.Viexport
+ * 
+ */
+Ext.namespace('App');
+
+App.Viewport = Ext.extend(Ext.Viewport, {
+
+    /**
+     * private shortcuts
+     */
+    tabPanel: null,
+    treePanel: null,
+    
+    /**
+     * constructor 
+     * @param {Object} config
+     */
+    constructor: function (config) {
+
+        var cgf,
+            loader,
+            root,
+            treePanel,
+            viewport;
+
+        Ext.apply(this, config);
+
+        // define the root node
+        root = new Ext.tree.AsyncTreeNode({
+            id: 'root',
+            nodeType: 'async',
+            expanded: true,
+            text: App.name
+        });
+
+        // define nodes loader
+        loader = new Ext.tree.TreeLoader({
+            directFn: Dbui.getTree
+        }); 
+
+        // predefined configuration of the view port
+        cfg = {
+            layout: 'border',
+            title: 'Ext Layout Browser',
+            items: [{
+                autoScroll: true,
+                collapsible: true,
+                itemId: 'treePanel',
+                loader: loader,
+                region: 'west',
+                root: root,
+                rootVisible: false,
+                split: true,
+                title: App.name,
+                width: 200,
+                xtype: 'treepanel'
+            }, {
+                autoScroll: true,
+                defaults: {layout: 'fit'},
+                itemId: 'tabPanel',
+                region: 'center',
+                xtype: 'tabpanel'
+            }]
+        };
+
+        // apply the user configuration if any
+        Ext.apply(this, cfg);
+        
+        // instanciate the viewport
+        App.Viewport.superclass.constructor.call(this);
+        
+        // define shortcuts
+        this.tabPanel = this.getComponent('tabPanel');
+        this.treePanel = this.getComponent('treePanel');
+        
+        // Add handler to create tab and their widget
+        this.treePanel.on('click', this.onCreateTab, this);
+        
+        // disable contextmenu in the treepanel
+        this.treePanel.on('contextmenu', function (node, event) {
+            event.stopEvent();
+        });
+    },
+
+    /**
+     * Handler to create / activate a tab
+     * The scope is the viewport
+     * 
+     * @param {Object} node
+     * @param {Object} event
+     */
+    onCreateTab: function(node, event){
+ 
+        var cfg,
+            parent = node.parentNode,
+            panel,
+            tabId,
+            viewport = this,
+            wdgcfg = node.attributes.cfg,
+            wdgtype = node.attributes.cfg.xtype;
+     
+        // protection
+        if(!node.isLeaf()){
+            return;
+        }   
+
+        // unique tab identifier
+        tabId = node.attributes.text + '/' + wdgtype;
+                
+        // activate an existing tab
+        if (this.tabPanel.getComponent(tabId)) {
+            this.tabPanel.setActiveTab(tabId);
+            return;
+        }
+
+        // tab configuration
+        // the title is defined as 'Form tablename', 'Grid tablename', ...
+        cfg = {
+            closable: true,
+            itemId: tabId,
+            title: parent.attributes.text + " " + node.attributes.text
+        };
+        
+        // embed single panel in the tab panel        
+        // other widget as children
+        if( wdgtype === 'panel') {
+            delete wdgcfg.xtype;
+            Ext.apply(cfg, wdgcfg);
+            
+        } else {
+            cfg.items = [node.attributes.cfg];
+        }
+        
+        // create a new tab and activate it
+        panel = this.tabPanel.add(cfg);
+        this.tabPanel.setActiveTab(tabId);
+    }
+});
diff --git a/static/plugin_dbui/dbui-min.js b/static/plugin_dbui/dbui-min.js
new file mode 100644
index 0000000000000000000000000000000000000000..337c4ca42cbbfed123f4176fb1d4227860006c6b
--- /dev/null
+++ b/static/plugin_dbui/dbui-min.js
@@ -0,0 +1 @@
+Ext.namespace("App");App.version="0.4.3";App.encodeField=function(b,d){var a,c;a=b[0].toUpperCase()+b.slice(1,b.length);c=d[0].toUpperCase()+d.slice(1,d.length);return a+c};App.getDirectStore=function(b){var a,c,d;c=Ext.StoreMgr.lookup(b);if(!c&&!Ext.isString(b)){throw new Error('Fail to instanciate the store: "'+b+'"')}if(!c&&Ext.isString(b)){d=b.slice(0,b.search("Store"));a=App.storeCfgs[d];c=new App.data.DirectStore(a)}return c};App.getTableName=function(a){return a.baseParams.tableName};App.isPlugin=function(a,d){var b,c;if(!a.hasOwnProperty("plugins")){return false}for(b=0;b<a.plugins.length;b+=1){c=a.plugins[b];if(c===d){return true}if((typeof(c)==="object")&&c.hasOwnProperty("ptype")&&(c.ptype===d)){return true}}return false};Ext.namespace("App");App.BasePanelWithSelector=Ext.extend(Ext.Panel,{panelCfg:null,selectorCfg:null,textGo:"Go",textReset:"Reset",constructor:function(d){var c,b,a;Ext.apply(this,d);b=Ext.ComponentMgr.create(this.panelCfg);a=Ext.ComponentMgr.create(this.selectorCfg);c={layout:"border",items:[{border:false,layout:"fit",itemId:"mainPanel",items:[b],region:"center",},{buttons:[{ref:"../../goButton",text:this.textGo},{ref:"../../resetButton",text:this.textReset}],collapsible:true,defaults:{anchor:"99%"},frame:true,layout:"form",itemId:"selectorPanel",items:[a],region:"east",split:true,width:300,}]};Ext.apply(this,c);App.BasePanelWithSelector.superclass.constructor.call(this)}});Ext.reg("xpanelwithselector",App.BasePanelWithSelector);Ext.namespace("App");App.ButtonDownload=Ext.extend(Ext.Button,{url:undefined,initComponent:function(){App.ButtonDownload.superclass.initComponent.call(this);this.on("click",this.onDownload,this)},onDownload:function(a,c){try{Ext.destroy(Ext.get("downloadIframe"))}catch(b){}Ext.DomHelper.append(document.body,{tag:"iframe",id:"downloadIframe",frameBorder:0,width:0,height:0,css:"display:none;visibility:hidden;height:0px;",src:this.url})}});Ext.reg("xbuttondownload",App.ButtonDownload);Ext.namespace("App.form");App.form.ComboBox=Ext.extend(Ext.form.ComboBox,{mode:"remote",editable:false,selectOnFocus:true,triggerAction:"all",typeAhead:true,initComponent:function(){if(this.fieldLabel&&!this.emptyText){this.emptyText="Select a "+this.fieldLabel+" ..."}this.store=App.getDirectStore(this.store);App.form.ComboBox.superclass.initComponent.call(this);if(this.store.getCount()===0){this.store.load()}this.store.on("load",function(){this.setValue(this.initialConfig.value)},this)}});Ext.reg("xcombobox",App.form.ComboBox);Ext.namespace("App.form");App.form.FormPanel=Ext.extend(Ext.form.FormPanel,{store:null,autoScroll:true,bodyStyle:"padding:5px 5px 0",buttons:[{formBind:true,text:"Action",ref:"../buttonAction"},{text:"Reset",ref:"../buttonReset"}],defaults:{anchor:"100%"},defaultType:"textfield",frame:true,monitorValid:true,currentAction:null,currentRecord:null,textCreate:"Create",textDestroy:"Delete",textDuplicate:"Duplicate",textReset:"Reset",textUpdate:"Update",initComponent:function(){function a(){this.reset()}App.form.FormPanel.superclass.initComponent.call(this);this.buttonAction.on("click",this.doAction,this);this.buttonReset.on("click",a,this.getForm());this.addFieldToolTipsListeners();this.store=App.getDirectStore(this.store);this.store.on("exception",this.onStoreException,this);this.store.on("write",a,this.getForm());this.setAction("create");this.buttonReset.setText(this.textReset)},addFieldToolTipsListeners:function(){var b;function a(d){new Ext.ToolTip({target:d.getEl(),title:d.fieldLabel,anchor:"left",trackMouse:false,html:Ext.util.Format.htmlEncode(d.tipText)})}b=this.getForm();b.items.each(function(c){if(c.tipText){c.on("render",a)}})},disableFields:function(a){var b=this.getForm();b.items.each(function(c){c.setDisabled(a)})},doAction:function(){var b=this.getForm(),a;if(!b.isValid()){return}if(!this.store){throw new Error("the store is undefined !!!")}switch(this.currentAction){case"create":a=new this.store.recordType();this.updateRecord(a);this.store.add(a);break;case"destroy":this.store.remove(this.currentRecord);break;case"duplicate":a=new this.store.recordType();this.updateRecord(a);this.store.add(a);break;case"update":this.currentRecord.beginEdit();this.updateRecord(this.currentRecord);this.currentRecord.endEdit();break}if(this.store.autoSave===false){this.store.save()}},hardReset:function(){var a=this.getForm();a.items.each(function(b){b.originalValue=Ext.value(b.initialConfig.value,"");b.setValue(b.originalValue)})},onStoreException:function(e,f,b,j,c,i){var g,h,a=this.getForm(),d;for(h in c.errors){if(c.errors.hasOwnProperty(h)){g=a.findField(h);g.markInvalid(c.errors[h])}}d=i[0];this.store.remove(d)},setAction:function(e,a){var d=this.getForm(),c,b;this.buttonReset.show();this.buttonAction.show();this.disableFields(false);this.currentAction=e;this.currentRecord=a;switch(e){case"create":this.hardReset();this.buttonAction.setText(this.textCreate);break;case"destroy":this.buttonAction.setText(this.textDestroy);d.loadRecord(a);break;case"duplicate":this.buttonAction.setText(this.textDuplicate);this.hardReset();c=App.getTableName(this.store);b=App.encodeField(c,"id");delete a.data[b];d.loadRecord(a);break;case"update":this.buttonAction.setText(this.textUpdate);d.loadRecord(a);break;case"view":this.buttonReset.hide();this.buttonAction.hide();d.loadRecord(a);this.disableFields(true);break}},updateRecord:function(b){var g,f,a=[],d,c,h,e;c=this.findByType("field");for(d=0;d<c.length;d+=1){f=c[d];if(f.getXType()!=="compositefield"){a.push(f)}}c=this.findByType("compositefield");for(d=0;d<c.length;d+=1){c[d].items.eachKey(function(j,i){a.push(i)})}for(d=0;d<a.length;d+=1){f=a[d];e=f.getValue();switch(f.getXType()){case"datefield":if(Ext.isDate(e)){e=e.format(f.format)}break;case"xcombobox":g=f;h=g.findRecord(g.valueField,g.getValue());b.set(g.displayField,h.get(g.displayField));break}b.set(f.getName(),e)}}});Ext.reg("xform",App.form.FormPanel);Ext.namespace("App.grid");App.grid.GridFilter=Ext.extend(Ext.form.FieldSet,{filterConditions:{},initialFilterConditions:[],pagingToolbar:null,store:null,defaults:{anchor:"99%",enableKeyEvents:true},initComponent:function(){var c,a,b;App.grid.GridFilter.superclass.initComponent.call(this);a=this.findByType("field");for(b=0;b<a.length;b+=1){c=a[b];if(c.xtype==="xcombobox"){c.on("select",this.onChange,this)}else{c.on("keyup",this.onChange,this,{buffer:500})}}},bind:function(b){var c,a;this.store=b.getStore();if(b.pagingInitialized){this.pagingToolbar=b.getBottomToolbar()}c=this.store.baseParams;for(a=0;a<c.where.length;a+=1){this.initialFilterConditions.push(c.where[a])}},onChange:function(a){this.setupCondition(a)},onReset:function(){var a,b;a=this.findByType("field");for(b=0;b<a.length;b+=1){a[b].reset()}this.filterConditions={};this.store.baseParams.where=this.initialFilterConditions;this.updateStore()},setupCondition:function(f){var e=[],c,a,d,b;d=f.getValue();b=f.name+" "+f.filterOperator+" '"+d+"'";if(d===""){delete this.filterConditions[f.name]}else{this.filterConditions[f.name]=b}for(c=0;c<this.initialFilterConditions.length;c+=1){e.push(this.initialFilterConditions[c])}for(a in this.filterConditions){if(this.filterConditions.hasOwnProperty(a)){e.push(this.filterConditions[a])}}this.store.baseParams.where=e;this.updateStore()},updateStore:function(){if(this.pagingToolbar){this.pagingToolbar.doRefresh()}else{this.store.load()}}});Ext.reg("xgridfilter",App.grid.GridFilter);Ext.namespace("App.grid");App.grid.Grid=Ext.extend(Ext.grid.GridPanel,{rowEditor:null,initComponent:function(){this.store=App.getDirectStore(this.store);this.bbar=new Ext.PagingToolbar();this.bbar.hide();App.grid.Grid.superclass.initComponent.call(this);if(!this.store.getTotalCount()){if(App.isPlugin(this,"pGridPaging")){this.store.load({params:{start:0,limit:10}})}else{this.store.load()}}}});Ext.reg("xgrid",App.grid.Grid);Ext.namespace("App.grid");App.grid.MathJax=Ext.extend(Object,{init:function(a){function b(d){var c=Ext.getDom(d.el);MathJax.Hub.Queue(["Typeset",MathJax.Hub,c])}a.getView().on("refresh",b,a);a.store.on("write",b,a)}});Ext.preg("pGridMathJax",App.grid.MathJax);Ext.namespace("App.grid");App.grid.Paging=Ext.extend(Object,{ptype:"pGridPaging",textExport:"Export",textSlider:"Rows per page",init:function(a){var b;b=a.getBottomToolbar();b.bindStore(a.store);b.add("-",this.textSlider,{xtype:"slider",plugins:new Ext.slider.Tip(),listeners:{changecomplete:this.onChangePageSize,scope:b},minValue:1,width:100},"->",{xtype:"xbuttondownload",text:this.textExport,url:App.csvUrl+"?tableName="+App.getTableName(a.store)});b.show();if(a.store.getTotalCount()>0){this.onInit.call(a,a.store)}else{a.store.on("load",this.onInit,a,{single:true})}a.store.on("write",this.onWrite,a)},onChangePageSize:function(b,c,a){var d=this;d.pageSize=c;d.moveFirst()},onInit:function(b,a,c){var g,e=this,d=b.getCount(),f;g=e.getBottomToolbar();g.pageSize=d;f=g.findByType("slider")[0];f.setMaxValue(b.getTotalCount());f.setValue(d)},onWrite:function(){var c,a=this,b;c=a.getBottomToolbar();c.pageSize=a.store.getCount();b=c.findByType("slider")[0];b.setMaxValue(a.store.getTotalCount());b.setValue(a.store.getCount())}});Ext.preg("pGridPaging",App.grid.Paging);Ext.namespace("App.grid");App.grid.RowEditorContextMenu=Ext.extend(Object,{ptype:"pGridRowEditorContextMenu",textAdd:"Add",textCreate:"Create",textDestroy:"Delete",textDuplicate:"Duplicate",textUpdate:"Update",textView:"View",init:function(a){var b=new Ext.menu.Menu();if(!a.rowEditor){throw new Error("no grid row editor !!!")}a.addListener("containercontextmenu",this.onContainerContextMenu,b);a.addListener("headercontextmenu",this.onHeaderContextMenu,b);a.addListener("rowcontextmenu",this.onRowContextMenu,b);b.add({text:this.textAdd,iconCls:"xaction-create",handler:a.rowEditor.onAddRow,scope:a.rowEditor},"-",{text:this.textDuplicate,iconCls:"xaction-duplicate",handler:a.rowEditor.onDuplicateRow,scope:a.rowEditor},{text:this.textUpdate,iconCls:"xaction-update",handler:a.rowEditor.onEditRow,scope:a.rowEditor},{text:this.textView,iconCls:"xaction-view",handler:a.rowEditor.onViewRow,scope:a.rowEditor},"-",{text:this.textDestroy,iconCls:"xaction-destroy",handler:a.rowEditor.onDeleteRow,scope:a.rowEditor})},onContainerContextMenu:function(a,b){var c=this;b.stopEvent();c.showAt(b.getXY())},onHeaderContextMenu:function(b,a,c){c.stopEvent()},onRowContextMenu:function(a,d,b){var c=this;b.stopEvent();a.selModel.selectRow(d);c.showAt(b.getXY())}});Ext.preg("pGridRowEditorContextMenu",App.grid.RowEditorContextMenu);Ext.namespace("App.grid");App.grid.RowEditor=Ext.extend(Ext.Window,{ptype:"pGridRowEditor",formPanel:null,grid:null,addTitle:"Create a new record...",deleteTitle:"Delete the record...",duplicateTitle:"Duplicate the record...",editTitle:"Update the record...",viewTitle:"View the record...",textMsg:"Select a row please",autoScroll:true,closeAction:"hide",constrainHeader:true,defaults:{autoScroll:true},modal:true,plain:true,init:function(a){var b;this.grid=a;this.grid.rowEditor=this;this.grid.selModel=new Ext.grid.RowSelectionModel({singleSelect:true});this.grid.store.on("write",this.onWrite,this);b=App.getTableName(this.grid.store);Dbui.getForm(b,this.addFormPanel,this)},addFormPanel:function(c,a){var b=a.result;this.formPanel=new App.form.FormPanel(b);this.add(this.formPanel);this.grid.store.on("exception",this.formPanel.onStoreException,this.formPanel)},getSelected:function(){var a=this.grid.getSelectionModel().getSelected();if(!a){Ext.MessageBox.alert("Warning",this.textMsg);return false}return a},onAddRow:function(){this.formPanel.setAction("create");this.setTitle(this.addTitle);this.show()},onDeleteRow:function(){var a=this.getSelected();if(!a){return}this.formPanel.setAction("destroy",a);this.setTitle(this.deleteTitle);this.formPanel.doAction()},onDuplicateRow:function(){var a=this.getSelected();if(!a){return}this.formPanel.setAction("duplicate",a);this.setTitle(this.duplicateTitle);this.show()},onEditRow:function(){var a=this.getSelected();if(!a){return}this.formPanel.setAction("update",a);this.setTitle(this.editTitle);this.show()},onViewRow:function(){var a=this.getSelected();if(!a){return}this.formPanel.setAction("view",a);this.setTitle(this.viewTitle);this.show()},onWrite:function(c,d,a,e,b){this.hide()}});Ext.preg("pGridRowEditor",App.grid.RowEditor);Ext.namespace("App.grid");App.grid.RowEditorToolbar=Ext.extend(Object,{ptype:"pGridRowEditorToolbar",init:function(a){var b;if(!a.rowEditor){throw new Error("no grid row editor !!!")}b=a.getTopToolbar();b.add([{text:"Add",iconCls:"silk-add",handler:a.rowEditor.onAddRow,scope:a.rowEditor}," ",{text:"Delete",iconCls:"silk-delete",handler:a.rowEditor.onDeleteRow,scope:a.rowEditor}," ",{text:"Duplicate",iconCls:"silk-clone",handler:a.rowEditor.onDuplicateRow,scope:a.rowEditor}," ",{text:"Update",iconCls:"silk-update",handler:a.rowEditor.onEditRow,scope:a.rowEditor}," ",{text:"View",iconCls:"silk-view",handler:a.rowEditor.onViewRow,scope:a.rowEditor},"-"])}});Ext.preg("pGridRowEditorToolbar",App.grid.RowEditorToolbar);Ext.namespace("App.grid");App.grid.GridWithFilter=Ext.extend(App.BasePanelWithSelector,{initComponent:function(){var b,a;this.items[1].collapsed=true;App.grid.GridWithFilter.superclass.initComponent.call(this);b=this.findByType("xgridfilter")[0];a=this.findByType("xgrid")[0];b.bind(a);this.goButton.hide();this.resetButton.on("click",b.onReset,b)}});Ext.reg("xgridwithfilter",App.grid.GridWithFilter);Ext.namespace("App.data");App.data.DirectStore=Ext.extend(Ext.data.DirectStore,{autoLoad:true,autoSave:true,constructor:function(c){var a,b;a=Ext.apply({},{encode:false,listful:true,writeAllFields:false},c);a=Ext.apply(a,{api:{create:Dbui.create,destroy:Dbui.destroy,read:Dbui.read,update:Dbui.update}});if(!Ext.isDefined(a.writer)){b=Ext.copyTo({},a,"encode, listful, writeAllFields");a.writer=new Ext.data.JsonWriter(b)}App.data.DirectStore.superclass.constructor.call(this,a);this.on("write",this.onWrite)},onWrite:function(a,b){switch(b){case"create":a.totalLength+=1;break;case"destroy":a.totalLength-=1;break}}});Ext.reg("xdirectstore",App.data.DirectStore);Ext.namespace("App.panel");App.panel.MathJax=Ext.extend(Object,{init:function(a){a.on("render",this.onPanelRender,this,{single:true})},onPanelRender:function(a){var b=a.body.getUpdater();b.on("update",this.onProcess)},onProcess:function(a,b){MathJax.Hub.PreProcess();MathJax.Hub.Process()}});Ext.preg("pPanelMathJax",App.panel.MathJax);Ext.namespace("App");App.PanelWithUrlSelector=Ext.extend(App.BasePanelWithSelector,{baseUrl:null,initComponent:function(){App.PanelWithUrlSelector.superclass.initComponent.call(this);this.goButton.on("click",this.onGo,this);this.resetButton.on("click",this.onReset,this)},onGo:function(){var b,d,c=this.getComponent("mainPanel"),e={},a=this.getComponent("selectorPanel");b=a.findByType("field");for(d=0;d<b.length;d+=1){e[b[d].getName()]=b[d].getValue()}c.load({url:this.baseUrl,params:e,text:"Loading...",timeout:30})},onReset:function(){var b,c,a=this.getComponent("selectorPanel");b=a.findByType("field");for(c=0;c<b.length;c+=1){b[c].reset()}}});Ext.reg("xpanelwithurlselector",App.PanelWithUrlSelector);Ext.namespace("App.form");App.form.SetBox=Ext.extend(Ext.form.ComboBox,{model:null,editable:false,emptyText:"Select...",selectOnFocus:true,triggerAction:"all",typeAhead:true,initComponent:function(){var b,a=[];if(!this.model){throw new Error("the property model is missing !!!")}a=[];for(b=0;b<this.model.setData.length;b+=1){a.push([this.model.setData[b]])}this.store=new Ext.data.ArrayStore({fields:[this.model.name],data:a});this.displayField=this.model.name;this.mode="local";App.form.SetBox.superclass.initComponent.call(this)}});Ext.reg("xsetbox",App.form.SetBox);Ext.namespace("App");App.Viewport=Ext.extend(Ext.Viewport,{tabPanel:null,treePanel:null,constructor:function(d){var e,b,c,f,a;Ext.apply(this,d);c=new Ext.tree.AsyncTreeNode({id:"root",nodeType:"async",expanded:true,text:App.name});b=new Ext.tree.TreeLoader({directFn:Dbui.getTree});cfg={layout:"border",title:"Ext Layout Browser",items:[{autoScroll:true,collapsible:true,itemId:"treePanel",loader:b,region:"west",root:c,rootVisible:false,split:true,title:App.name,width:200,xtype:"treepanel"},{autoScroll:true,defaults:{layout:"fit"},itemId:"tabPanel",region:"center",xtype:"tabpanel"}]};Ext.apply(this,cfg);App.Viewport.superclass.constructor.call(this);this.tabPanel=this.getComponent("tabPanel");this.treePanel=this.getComponent("treePanel");this.treePanel.on("click",this.onCreateTab,this);this.treePanel.on("contextmenu",function(h,g){g.stopEvent()})},onCreateTab:function(d,b){var f,h=d.parentNode,a,c,g=this,e=d.attributes.cfg,i=d.attributes.cfg.xtype;if(!d.isLeaf()){return}c=d.attributes.text+"/"+i;if(this.tabPanel.getComponent(c)){this.tabPanel.setActiveTab(c);return}f={closable:true,itemId:c,title:h.attributes.text+" "+d.attributes.text};if(i==="panel"){delete e.xtype;Ext.apply(f,e)}else{f.items=[d.attributes.cfg]}a=this.tabPanel.add(f);this.tabPanel.setActiveTab(c)}});
\ No newline at end of file
diff --git a/static/plugin_dbui/dbui.min.js b/static/plugin_dbui/dbui.min.js
deleted file mode 100644
index a1e34ca572f8285c081d53390ff381f8d3e72754..0000000000000000000000000000000000000000
--- a/static/plugin_dbui/dbui.min.js
+++ /dev/null
@@ -1 +0,0 @@
-Ext.namespace("App.grid");App.grid.MathJax=Ext.extend(Object,{init:function(a){function b(d){var c=Ext.getDom(d.el);MathJax.Hub.Queue(["Typeset",MathJax.Hub,c])}a.getView().on("refresh",b,a);a.store.on("write",b,a)}});Ext.preg("pGridMathJax",App.grid.MathJax);Ext.namespace("App.grid");App.grid.Grid=Ext.extend(Ext.grid.GridPanel,{rowEditor:null,initComponent:function(){this.store=App.getDirectStore(this.store);this.bbar=new Ext.PagingToolbar();this.bbar.hide();App.grid.Grid.superclass.initComponent.call(this);if(!this.store.getTotalCount()){if(App.isPlugin(this,"pGridPaging")){this.store.load({params:{start:0,limit:10}})}else{this.store.load()}}}});Ext.reg("xgrid",App.grid.Grid);Ext.namespace("App");App.version="0.4.3";App.encodeField=function(b,d){var a,c;a=b[0].toUpperCase()+b.slice(1,b.length);c=d[0].toUpperCase()+d.slice(1,d.length);return a+c};App.getDirectStore=function(b){var a,c,d;c=Ext.StoreMgr.lookup(b);if(!c&&!Ext.isString(b)){throw new Error('Fail to instanciate the store: "'+b+'"')}if(!c&&Ext.isString(b)){d=b.slice(0,b.search("Store"));a=App.storeCfgs[d];c=new App.data.DirectStore(a)}return c};App.getTableName=function(a){return a.baseParams.tableName};App.isPlugin=function(a,d){var b,c;if(!a.hasOwnProperty("plugins")){return false}for(b=0;b<a.plugins.length;b+=1){c=a.plugins[b];if(c===d){return true}if((typeof(c)==="object")&&c.hasOwnProperty("ptype")&&(c.ptype===d)){return true}}return false};Ext.namespace("App.grid");App.grid.Paging=Ext.extend(Object,{ptype:"pGridPaging",textExport:"Export",textSlider:"Rows per page",init:function(a){var b;b=a.getBottomToolbar();b.bindStore(a.store);b.add("-",this.textSlider,{xtype:"slider",plugins:new Ext.slider.Tip(),listeners:{changecomplete:this.onChangePageSize,scope:b},minValue:1,width:100},"->",{xtype:"xbuttondownload",text:this.textExport,url:App.csvUrl+"?tableName="+App.getTableName(a.store)});b.show();if(a.store.getTotalCount()>0){this.onInit.call(a,a.store)}else{a.store.on("load",this.onInit,a,{single:true})}a.store.on("write",this.onWrite,a)},onChangePageSize:function(b,c,a){var d=this;d.pageSize=c;d.moveFirst()},onInit:function(b,a,c){var g,e=this,d=b.getCount(),f;g=e.getBottomToolbar();g.pageSize=d;f=g.findByType("slider")[0];f.setMaxValue(b.getTotalCount());f.setValue(d)},onWrite:function(){var c,a=this,b;c=a.getBottomToolbar();c.pageSize=a.store.getCount();b=c.findByType("slider")[0];b.setMaxValue(a.store.getTotalCount());b.setValue(a.store.getCount())}});Ext.preg("pGridPaging",App.grid.Paging);Ext.namespace("App.grid");App.grid.RowEditor=Ext.extend(Ext.Window,{ptype:"pGridRowEditor",formPanel:null,grid:null,addTitle:"Create a new record...",deleteTitle:"Delete the record...",duplicateTitle:"Duplicate the record...",editTitle:"Update the record...",viewTitle:"View the record...",textMsg:"Select a row please",autoScroll:true,closeAction:"hide",constrainHeader:true,defaults:{autoScroll:true},modal:true,plain:true,init:function(a){var b;this.grid=a;this.grid.rowEditor=this;this.grid.selModel=new Ext.grid.RowSelectionModel({singleSelect:true});this.grid.store.on("write",this.onWrite,this);b=App.getTableName(this.grid.store);Dbui.getForm(b,this.addFormPanel,this)},addFormPanel:function(c,a){var b=a.result;this.formPanel=new App.form.FormPanel(b);this.add(this.formPanel);this.grid.store.on("exception",this.formPanel.onStoreException,this.formPanel)},getSelected:function(){var a=this.grid.getSelectionModel().getSelected();if(!a){Ext.MessageBox.alert("Warning",this.textMsg);return false}return a},onAddRow:function(){this.formPanel.setAction("create");this.setTitle(this.addTitle);this.show()},onDeleteRow:function(){var a=this.getSelected();if(!a){return}this.formPanel.setAction("destroy",a);this.setTitle(this.deleteTitle);this.formPanel.doAction()},onDuplicateRow:function(){var a=this.getSelected();if(!a){return}this.formPanel.setAction("duplicate",a);this.setTitle(this.duplicateTitle);this.show()},onEditRow:function(){var a=this.getSelected();if(!a){return}this.formPanel.setAction("update",a);this.setTitle(this.editTitle);this.show()},onViewRow:function(){var a=this.getSelected();if(!a){return}this.formPanel.setAction("view",a);this.setTitle(this.viewTitle);this.show()},onWrite:function(c,d,a,e,b){this.hide()}});Ext.preg("pGridRowEditor",App.grid.RowEditor);Ext.namespace("App");App.ButtonDownload=Ext.extend(Ext.Button,{url:undefined,initComponent:function(){App.ButtonDownload.superclass.initComponent.call(this);this.on("click",this.onDownload,this)},onDownload:function(a,c){try{Ext.destroy(Ext.get("downloadIframe"))}catch(b){}Ext.DomHelper.append(document.body,{tag:"iframe",id:"downloadIframe",frameBorder:0,width:0,height:0,css:"display:none;visibility:hidden;height:0px;",src:this.url})}});Ext.reg("xbuttondownload",App.ButtonDownload);Ext.namespace("App.form");App.form.FormPanel=Ext.extend(Ext.form.FormPanel,{store:null,autoScroll:true,bodyStyle:"padding:5px 5px 0",buttons:[{formBind:true,text:"Action",ref:"../buttonAction"},{text:"Reset",ref:"../buttonReset"}],defaults:{anchor:"100%"},defaultType:"textfield",frame:true,monitorValid:true,currentAction:null,currentRecord:null,textCreate:"Create",textDestroy:"Delete",textDuplicate:"Duplicate",textReset:"Reset",textUpdate:"Update",initComponent:function(){function a(){this.reset()}App.form.FormPanel.superclass.initComponent.call(this);this.buttonAction.on("click",this.doAction,this);this.buttonReset.on("click",a,this.getForm());this.addFieldToolTipsListeners();this.store=App.getDirectStore(this.store);this.store.on("exception",this.onStoreException,this);this.store.on("write",a,this.getForm());this.setAction("create");this.buttonReset.setText(this.textReset)},addFieldToolTipsListeners:function(){var b;function a(d){new Ext.ToolTip({target:d.getEl(),title:d.fieldLabel,anchor:"left",trackMouse:false,html:Ext.util.Format.htmlEncode(d.tipText)})}b=this.getForm();b.items.each(function(c){if(c.tipText){c.on("render",a)}})},disableFields:function(a){var b=this.getForm();b.items.each(function(c){c.setDisabled(a)})},doAction:function(){var b=this.getForm(),a;if(!b.isValid()){return}if(!this.store){throw new Error("the store is undefined !!!")}switch(this.currentAction){case"create":a=new this.store.recordType();this.updateRecord(a);this.store.add(a);break;case"destroy":this.store.remove(this.currentRecord);break;case"duplicate":a=new this.store.recordType();this.updateRecord(a);this.store.add(a);break;case"update":this.currentRecord.beginEdit();this.updateRecord(this.currentRecord);this.currentRecord.endEdit();break}if(this.store.autoSave===false){this.store.save()}},hardReset:function(){var a=this.getForm();a.items.each(function(b){b.originalValue=Ext.value(b.initialConfig.value,"");b.setValue(b.originalValue)})},onStoreException:function(e,f,b,j,c,i){var g,h,a=this.getForm(),d;for(h in c.errors){if(c.errors.hasOwnProperty(h)){g=a.findField(h);g.markInvalid(c.errors[h])}}d=i[0];this.store.remove(d)},setAction:function(e,a){var d=this.getForm(),c,b;this.buttonReset.show();this.buttonAction.show();this.disableFields(false);this.currentAction=e;this.currentRecord=a;switch(e){case"create":this.hardReset();this.buttonAction.setText(this.textCreate);break;case"destroy":this.buttonAction.setText(this.textDestroy);d.loadRecord(a);break;case"duplicate":this.buttonAction.setText(this.textDuplicate);this.hardReset();c=App.getTableName(this.store);b=App.encodeField(c,"id");delete a.data[b];d.loadRecord(a);break;case"update":this.buttonAction.setText(this.textUpdate);d.loadRecord(a);break;case"view":this.buttonReset.hide();this.buttonAction.hide();d.loadRecord(a);this.disableFields(true);break}},updateRecord:function(b){var g,f,a=[],d,c,h,e;c=this.findByType("field");for(d=0;d<c.length;d+=1){f=c[d];if(f.getXType()!=="compositefield"){a.push(f)}}c=this.findByType("compositefield");for(d=0;d<c.length;d+=1){c[d].items.eachKey(function(j,i){a.push(i)})}for(d=0;d<a.length;d+=1){f=a[d];e=f.getValue();switch(f.getXType()){case"datefield":if(Ext.isDate(e)){e=e.format(f.format)}break;case"xcombobox":g=f;h=g.findRecord(g.valueField,g.getValue());b.set(g.displayField,h.get(g.displayField));break}b.set(f.getName(),e)}}});Ext.reg("xform",App.form.FormPanel);Ext.namespace("App");App.Viewport=Ext.extend(Ext.Viewport,{tabPanel:null,treePanel:null,constructor:function(d){var e,b,c,f,a;Ext.apply(this,d);c=new Ext.tree.AsyncTreeNode({id:"root",nodeType:"async",expanded:true,text:App.name});b=new Ext.tree.TreeLoader({directFn:Dbui.getTree});cfg={layout:"border",title:"Ext Layout Browser",items:[{autoScroll:true,collapsible:true,itemId:"treePanel",loader:b,region:"west",root:c,rootVisible:false,split:true,title:App.name,width:200,xtype:"treepanel"},{autoScroll:true,defaults:{layout:"fit"},itemId:"tabPanel",region:"center",xtype:"tabpanel"}]};Ext.apply(this,cfg);App.Viewport.superclass.constructor.call(this);this.tabPanel=this.getComponent("tabPanel");this.treePanel=this.getComponent("treePanel");this.treePanel.on("click",this.onCreateTab,this);this.treePanel.on("contextmenu",function(h,g){g.stopEvent()})},onCreateTab:function(d,b){var f,h=d.parentNode,a,c,g=this,e=d.attributes.cfg,i=d.attributes.cfg.xtype;if(!d.isLeaf()){return}c=d.attributes.text+"/"+i;if(this.tabPanel.getComponent(c)){this.tabPanel.setActiveTab(c);return}f={closable:true,itemId:c,title:h.attributes.text+" "+d.attributes.text};if(i==="panel"){delete e.xtype;Ext.apply(f,e)}else{f.items=[d.attributes.cfg]}a=this.tabPanel.add(f);this.tabPanel.setActiveTab(c)}});Ext.namespace("App");App.BasePanelWithSelector=Ext.extend(Ext.Panel,{panelCfg:null,selectorCfg:null,textGo:"Go",textReset:"Reset",constructor:function(d){var c,b,a;Ext.apply(this,d);b=Ext.ComponentMgr.create(this.panelCfg);a=Ext.ComponentMgr.create(this.selectorCfg);c={layout:"border",items:[{border:false,layout:"fit",itemId:"mainPanel",items:[b],region:"center",},{buttons:[{ref:"../../goButton",text:this.textGo},{ref:"../../resetButton",text:this.textReset}],collapsible:true,defaults:{anchor:"99%"},frame:true,layout:"form",itemId:"selectorPanel",items:[a],region:"east",split:true,width:300,}]};Ext.apply(this,c);App.BasePanelWithSelector.superclass.constructor.call(this)}});Ext.reg("xpanelwithselector",App.BasePanelWithSelector);Ext.namespace("App.form");App.form.ComboBox=Ext.extend(Ext.form.ComboBox,{mode:"remote",editable:false,selectOnFocus:true,triggerAction:"all",typeAhead:true,initComponent:function(){if(this.fieldLabel&&!this.emptyText){this.emptyText="Select a "+this.fieldLabel+" ..."}this.store=App.getDirectStore(this.store);App.form.ComboBox.superclass.initComponent.call(this);if(this.store.getCount()===0){this.store.load()}this.store.on("load",function(){this.setValue(this.initialConfig.value)},this)}});Ext.reg("xcombobox",App.form.ComboBox);Ext.namespace("App.grid");App.grid.GridWithFilter=Ext.extend(App.BasePanelWithSelector,{initComponent:function(){var b,a;this.items[1].collapsed=true;App.grid.GridWithFilter.superclass.initComponent.call(this);b=this.findByType("xgridfilter")[0];a=this.findByType("xgrid")[0];b.bind(a);this.goButton.hide();this.resetButton.on("click",b.onReset,b)}});Ext.reg("xgridwithfilter",App.grid.GridWithFilter);Ext.namespace("App.form");App.form.SetBox=Ext.extend(Ext.form.ComboBox,{model:null,editable:false,emptyText:"Select...",selectOnFocus:true,triggerAction:"all",typeAhead:true,initComponent:function(){var b,a=[];if(!this.model){throw new Error("the property model is missing !!!")}a=[];for(b=0;b<this.model.setData.length;b+=1){a.push([this.model.setData[b]])}this.store=new Ext.data.ArrayStore({fields:[this.model.name],data:a});this.displayField=this.model.name;this.mode="local";App.form.SetBox.superclass.initComponent.call(this)}});Ext.reg("xsetbox",App.form.SetBox);Ext.namespace("App.grid");App.grid.RowEditorToolbar=Ext.extend(Object,{ptype:"pGridRowEditorToolbar",init:function(a){var b;if(!a.rowEditor){throw new Error("no grid row editor !!!")}b=a.getTopToolbar();b.add([{text:"Add",iconCls:"silk-add",handler:a.rowEditor.onAddRow,scope:a.rowEditor}," ",{text:"Delete",iconCls:"silk-delete",handler:a.rowEditor.onDeleteRow,scope:a.rowEditor}," ",{text:"Duplicate",iconCls:"silk-clone",handler:a.rowEditor.onDuplicateRow,scope:a.rowEditor}," ",{text:"Update",iconCls:"silk-update",handler:a.rowEditor.onEditRow,scope:a.rowEditor}," ",{text:"View",iconCls:"silk-view",handler:a.rowEditor.onViewRow,scope:a.rowEditor},"-"])}});Ext.preg("pGridRowEditorToolbar",App.grid.RowEditorToolbar);Ext.namespace("App.grid");App.grid.RowEditorContextMenu=Ext.extend(Object,{ptype:"pGridRowEditorContextMenu",textAdd:"Add",textCreate:"Create",textDestroy:"Delete",textDuplicate:"Duplicate",textUpdate:"Update",textView:"View",init:function(a){var b=new Ext.menu.Menu();if(!a.rowEditor){throw new Error("no grid row editor !!!")}a.addListener("containercontextmenu",this.onContainerContextMenu,b);a.addListener("headercontextmenu",this.onHeaderContextMenu,b);a.addListener("rowcontextmenu",this.onRowContextMenu,b);b.add({text:this.textAdd,iconCls:"xaction-create",handler:a.rowEditor.onAddRow,scope:a.rowEditor},"-",{text:this.textDuplicate,iconCls:"xaction-duplicate",handler:a.rowEditor.onDuplicateRow,scope:a.rowEditor},{text:this.textUpdate,iconCls:"xaction-update",handler:a.rowEditor.onEditRow,scope:a.rowEditor},{text:this.textView,iconCls:"xaction-view",handler:a.rowEditor.onViewRow,scope:a.rowEditor},"-",{text:this.textDestroy,iconCls:"xaction-destroy",handler:a.rowEditor.onDeleteRow,scope:a.rowEditor})},onContainerContextMenu:function(a,b){var c=this;b.stopEvent();c.showAt(b.getXY())},onHeaderContextMenu:function(b,a,c){c.stopEvent()},onRowContextMenu:function(a,d,b){var c=this;b.stopEvent();a.selModel.selectRow(d);c.showAt(b.getXY())}});Ext.preg("pGridRowEditorContextMenu",App.grid.RowEditorContextMenu);Ext.namespace("App.grid");App.grid.GridFilter=Ext.extend(Ext.form.FieldSet,{filterConditions:{},initialFilterConditions:[],pagingToolbar:null,store:null,defaults:{anchor:"99%",enableKeyEvents:true},initComponent:function(){var c,a,b;App.grid.GridFilter.superclass.initComponent.call(this);a=this.findByType("field");for(b=0;b<a.length;b+=1){c=a[b];if(c.xtype==="xcombobox"){c.on("select",this.onChange,this)}else{c.on("keyup",this.onChange,this,{buffer:500})}}},bind:function(b){var c,a;this.store=b.getStore();if(b.pagingInitialized){this.pagingToolbar=b.getBottomToolbar()}c=this.store.baseParams;for(a=0;a<c.where.length;a+=1){this.initialFilterConditions.push(c.where[a])}},onChange:function(a){this.setupCondition(a)},onReset:function(){var a,b;a=this.findByType("field");for(b=0;b<a.length;b+=1){a[b].reset()}this.filterConditions={};this.store.baseParams.where=this.initialFilterConditions;this.updateStore()},setupCondition:function(f){var e=[],c,a,d,b;d=f.getValue();b=f.name+" "+f.filterOperator+" '"+d+"'";if(d===""){delete this.filterConditions[f.name]}else{this.filterConditions[f.name]=b}for(c=0;c<this.initialFilterConditions.length;c+=1){e.push(this.initialFilterConditions[c])}for(a in this.filterConditions){if(this.filterConditions.hasOwnProperty(a)){e.push(this.filterConditions[a])}}this.store.baseParams.where=e;this.updateStore()},updateStore:function(){if(this.pagingToolbar){this.pagingToolbar.doRefresh()}else{this.store.load()}}});Ext.reg("xgridfilter",App.grid.GridFilter);Ext.namespace("App.panel");App.panel.MathJax=Ext.extend(Object,{init:function(a){a.on("render",this.onPanelRender,this,{single:true})},onPanelRender:function(a){var b=a.body.getUpdater();b.on("update",this.onProcess)},onProcess:function(a,b){MathJax.Hub.PreProcess();MathJax.Hub.Process()}});Ext.preg("pPanelMathJax",App.panel.MathJax);Ext.namespace("App.data");App.data.DirectStore=Ext.extend(Ext.data.DirectStore,{autoLoad:true,autoSave:true,constructor:function(c){var a,b;a=Ext.apply({},{encode:false,listful:true,writeAllFields:false},c);a=Ext.apply(a,{api:{create:Dbui.create,destroy:Dbui.destroy,read:Dbui.read,update:Dbui.update}});if(!Ext.isDefined(a.writer)){b=Ext.copyTo({},a,"encode, listful, writeAllFields");a.writer=new Ext.data.JsonWriter(b)}App.data.DirectStore.superclass.constructor.call(this,a);this.on("write",this.onWrite)},onWrite:function(a,b){switch(b){case"create":a.totalLength+=1;break;case"destroy":a.totalLength-=1;break}}});Ext.reg("xdirectstore",App.data.DirectStore);Ext.namespace("App");App.PanelWithUrlSelector=Ext.extend(App.BasePanelWithSelector,{baseUrl:null,initComponent:function(){App.PanelWithUrlSelector.superclass.initComponent.call(this);this.goButton.on("click",this.onGo,this);this.resetButton.on("click",this.onReset,this)},onGo:function(){var b,d,c=this.getComponent("mainPanel"),e={},a=this.getComponent("selectorPanel");b=a.findByType("field");for(d=0;d<b.length;d+=1){e[b[d].getName()]=b[d].getValue()}c.load({url:this.baseUrl,params:e,text:"Loading...",timeout:30})},onReset:function(){var b,c,a=this.getComponent("selectorPanel");b=a.findByType("field");for(c=0;c<b.length;c+=1){b[c].reset()}}});Ext.reg("xpanelwithurlselector",App.PanelWithUrlSelector);
\ No newline at end of file
diff --git a/views/plugin_dbui/index.html b/views/plugin_dbui/index.html
index 2ee502a6b094d0c263ec8d00c36fe4df36bd8e6a..da0645a6a51875370a87e3104c982689f86152da 100644
--- a/views/plugin_dbui/index.html
+++ b/views/plugin_dbui/index.html
@@ -36,7 +36,7 @@
 
         <!-- dbui + user javascript library and main script -->
         <script type="text/javascript" src="/{{=request.application}}/plugin_dbui/get_api"></script>
-        <script type="text/javascript" src="/{{=request.application}}/static/plugin_dbui/dbui.min.js"></script>
+        <script type="text/javascript" src="/{{=request.application}}/static/plugin_dbui/dbui-min.js"></script>
         <script type="text/javascript" src="/{{=request.application}}/static/plugin_dbui/locale/app-lang-{{=lg}}.js"></script>
         {{for el in response.files:}}<script type="text/javascript" src="{{=el}}"></script>{{pass}}