This manual is deprecated. Please visit https://groupoffice.readthedocs.io for the latest documentation. |
Difference between revisions of "Creating a module"
(→Designing the views) |
|||
(24 intermediate revisions by 3 users not shown) | |||
Line 1: | Line 1: | ||
− | + | == Creating an empty module == | |
− | + | Now we will create the main module class. | |
− | + | A Group-Office module will extend the GO_Base_Module class. | |
− | + | Create the file module/presidents/PresidentsModule.php | |
− | + | '''PresidentsModule.php:''' | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
<pre> | <pre> | ||
− | + | class GO_Presidents_PresidentsModule extends GO_Base_Module{ | |
− | + | ||
+ | public function autoInstall() { | ||
+ | return true; | ||
+ | } | ||
+ | |||
+ | public function author() { | ||
+ | return 'Merijn Schering'; | ||
+ | } | ||
+ | |||
+ | public function authorEmail() { | ||
+ | return 'mschering@intermesh.nl'; | ||
+ | } | ||
+ | } | ||
</pre> | </pre> | ||
− | + | === Creating our first view === | |
− | + | Now we will create the main panel of the module. | |
− | + | All views in Group-Office view is an extension of an ExtJS Panel. So it can be put in any other ExtJS Container. | |
− | + | ||
− | + | For the main panel create the file: module/presidents/views/Extjs3/MainPanel.js | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
'''MainPanel.js:''' | '''MainPanel.js:''' | ||
<pre> | <pre> | ||
// Creates namespaces to be used for scoping variables and classes so that they are not global. | // Creates namespaces to be used for scoping variables and classes so that they are not global. | ||
− | Ext.namespace('GO. | + | Ext.namespace('GO.presidents'); |
/* | /* | ||
* This is the constructor of our MainPanel | * This is the constructor of our MainPanel | ||
*/ | */ | ||
− | GO. | + | GO.presidents.MainPanel = function(config){ |
if(!config) | if(!config) | ||
Line 51: | Line 52: | ||
* Explicitly call the superclass constructor | * Explicitly call the superclass constructor | ||
*/ | */ | ||
− | GO. | + | GO.presidents.MainPanel.superclass.constructor.call(this, config); |
} | } | ||
/* | /* | ||
− | * Extend the | + | * Extend our MainPanel from the ExtJS Panel |
*/ | */ | ||
− | Ext.extend(GO. | + | Ext.extend(GO.presidents.MainPanel, Ext.Panel,{ |
}); | }); | ||
/* | /* | ||
− | * This will add the module to the main | + | * This will add the module to the main tab-panel filled with all the modules |
*/ | */ | ||
− | GO.moduleManager.addModule(' | + | GO.moduleManager.addModule( |
− | title : ' | + | 'presidents', //Module alias |
− | + | GO.presidents.MainPanel, //The main panel for this module | |
− | }); | + | { |
+ | title : 'Presidents', //Module name in start-menu | ||
+ | iconCls : 'go-module-icon-presidents' //The css class with icon for start-menu | ||
+ | } | ||
+ | ); | ||
+ | |||
</pre> | </pre> | ||
− | + | This view will only render the text 'Hello World' | |
− | For | + | For the main panel to be loaded when the module is active, we'll need to create a file called scripts.txt in our /modules/presidents/views/ExtJs3/ folder. |
'''scripts.txt:''' | '''scripts.txt:''' | ||
<pre> | <pre> | ||
− | modules/ | + | modules/presidents/views/Extjs3/MainPanel.js |
</pre> | </pre> | ||
Without this file your module will never work! | Without this file your module will never work! | ||
− | + | === Multilingual functions === | |
− | + | To make the module translatable create the follow file: modules/presidents/language/en.php: | |
− | + | '''en.php''' | |
+ | <pre> | ||
+ | <?php | ||
+ | /** | ||
+ | * This $l array excists of key-value pairs | ||
+ | * key is what should be called in the view eg. GO.presidents.name | ||
+ | * value is the string that will be displayed in your view | ||
+ | */ | ||
+ | $l = array( | ||
+ | 'name' => ' Presidents', //this name will appear in the Start menu -> modules section | ||
+ | 'description' => 'A simple test module', //this description will appear in the Start menu -> modules section | ||
+ | ); | ||
+ | </pre> | ||
+ | These two string are the name and the description of the Group-Office module and are use by the module installation dialog. | ||
+ | |||
+ | After completing these steps you should be able to install your module and be able to display the hello world view that was created. | ||
+ | |||
+ | In Group-Office go to 'Admin menu->modules' and install your newly created module. | ||
== The Tutorial == | == The Tutorial == | ||
Line 95: | Line 118: | ||
The example module we are going to make will show you how to create a simple module that displays data from a database and lets us add items to it, change them and delete them. | The example module we are going to make will show you how to create a simple module that displays data from a database and lets us add items to it, change them and delete them. | ||
− | All files are also available | + | All files are also available in the Group-Office download. You can find them in modules/presidents/. |
=== Before we start === | === Before we start === | ||
− | First, we need to | + | First, we need to create the database containing the data we are going to work with. |
− | + | We will create 2 tables named 'pm_parties' and 'pm_presidents' | |
+ | It is always a good idea to prefix the database tables used for a module (pm_ is short for President Module) | ||
+ | |||
+ | Run the following MySQL query on the Group-Office database of your installation: | ||
<pre> | <pre> | ||
− | CREATE TABLE IF NOT EXISTS ` | + | CREATE TABLE IF NOT EXISTS `pm_parties` ( |
− | ` | + | `id` int(11) NOT NULL auto_increment, |
− | ` | + | `name` varchar(40) NOT NULL, |
− | PRIMARY KEY (` | + | PRIMARY KEY (`id`) |
); | ); | ||
− | INSERT INTO ` | + | INSERT INTO `pm_parties` (`id`, `name`) VALUES |
(1, 'No Party'), | (1, 'No Party'), | ||
(2, 'Federalist'), | (2, 'Federalist'), | ||
Line 117: | Line 143: | ||
(6, 'Republican'); | (6, 'Republican'); | ||
− | CREATE TABLE IF NOT EXISTS ` | + | CREATE TABLE IF NOT EXISTS `pm_presidents` ( |
`id` int(11) NOT NULL auto_increment, | `id` int(11) NOT NULL auto_increment, | ||
− | ` | + | `party_id` int(11) NOT NULL, |
`firstname` varchar(20) NOT NULL, | `firstname` varchar(20) NOT NULL, | ||
`lastname` varchar(20) NOT NULL, | `lastname` varchar(20) NOT NULL, | ||
Line 125: | Line 151: | ||
`leftoffice` date NOT NULL, | `leftoffice` date NOT NULL, | ||
`income` decimal(14,2) NOT NULL, | `income` decimal(14,2) NOT NULL, | ||
+ | `ctime` int(11) NOT NULL DEFAULT '0', | ||
+ | `mtime` int(11) NOT NULL DEFAULT '0', | ||
PRIMARY KEY (`id`) | PRIMARY KEY (`id`) | ||
); | ); | ||
− | INSERT INTO ` | + | INSERT INTO `pm_presidents` (`id`, `party_id`, `firstname`, `lastname`, `tookoffice`, `leftoffice`, `income`) VALUES |
(1, 1, 'George', 'Washington', '1789-04-30', '1797-03-04', 135246.32), | (1, 1, 'George', 'Washington', '1789-04-30', '1797-03-04', 135246.32), | ||
(2, 2, 'John', 'Adams', '1797-03-04', '1801-03-04', 236453.34), | (2, 2, 'John', 'Adams', '1797-03-04', '1801-03-04', 236453.34), | ||
Line 173: | Line 201: | ||
</pre> | </pre> | ||
− | + | <b>Note:</b> We can insert this data manually into the database. Or this script can be executed automatically when installing the module. (See section Installation and upgrades) | |
− | + | === Building the models === | |
− | + | ||
− | + | ||
− | + | We have 2 types of data that is going to be used on this module '''President''' and '''Party''' | |
+ | For this we will create to Model classes in the modules/presidents/models folder | ||
− | + | '''Party.php''' | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | ''' | + | |
<pre> | <pre> | ||
<?php | <?php | ||
+ | /** | ||
+ | * The Party model | ||
+ | * | ||
+ | * @property int $id | ||
+ | * @property string $name | ||
+ | */ | ||
+ | class GO_Presidents_Model_Party extends GO_Base_Db_ActiveRecord { | ||
+ | /** | ||
+ | * Returns a static model of itself | ||
+ | * | ||
+ | * @param String $className | ||
+ | * @return GO_Presidents_Model_Party | ||
+ | */ | ||
+ | public static function model($className=__CLASS__) | ||
+ | { | ||
+ | return parent::model($className); | ||
+ | } | ||
− | + | public function tableName() { | |
+ | return 'pm_parties'; | ||
+ | } | ||
− | + | public function relations() { | |
− | + | return array( | |
+ | 'presidents' => array('type' => self::HAS_MANY, 'model' => 'GO_Presidents_Model_President', 'field' => 'party_id', 'delete' => true) | ||
+ | ); | ||
+ | } | ||
+ | } | ||
+ | </pre> | ||
− | + | Our model class will extend from GO_Base_Db_ActiveRecord. Also the name of the class is important and should be in the following format: '''GO_[modulename]_Model_[modelname]''' where model name should be the same as the filename. | |
− | + | ||
− | + | ||
− | + | This is all there is to create a model class. The properties of the model are already known since the database table is specified in the tableName() function | |
− | + | Also a relation is defined between the Party Model and the President model. | |
+ | Create the president model as follows: | ||
− | + | '''President.php''' | |
− | { | + | <pre> |
+ | <?php | ||
+ | /** | ||
+ | * The President model | ||
+ | * | ||
+ | * @property int $id | ||
+ | * @property int $party_id | ||
+ | * @property string $firstname | ||
+ | * @property string $lastame | ||
+ | * @property date $tookoffice | ||
+ | * @property date $leftoffice | ||
+ | * @property float $income | ||
+ | */ | ||
+ | class GO_Presidents_Model_President extends GO_Base_Db_ActiveRecord { | ||
+ | |||
+ | /** | ||
+ | * Returns a static model of itself | ||
+ | * | ||
+ | * @param String $className | ||
+ | * @return GO_Presidents_Model_President | ||
+ | */ | ||
+ | public static function model($className=__CLASS__) | ||
+ | { | ||
+ | return parent::model($className); | ||
+ | } | ||
+ | |||
+ | protected function init() { | ||
+ | $this->columns["income"]["gotype"]="number"; | ||
+ | return parent::init(); | ||
+ | } | ||
+ | |||
+ | public function tableName(){ | ||
+ | return 'pm_presidents'; | ||
+ | } | ||
− | + | public function relations(){ | |
− | + | return array( | |
− | + | 'party' => array('type'=>self::BELONGS_TO, 'model'=>'GO_Presidents_Model_Party', 'field'=>'party_id'), ); | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
} | } | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
} | } | ||
− | |||
− | |||
− | |||
</pre> | </pre> | ||
− | ' | + | That's all there is to creating models for this example module |
− | + | ||
− | + | ||
− | + | === Creating the controllers === | |
− | + | The controllers we stick into the 'modules/presidents/controller' folder | |
− | + | ||
− | + | Create the following 2 files: | |
− | + | ||
− | + | ||
− | + | '''PartyController.php''' | |
− | + | <pre> | |
− | + | <?php | |
− | + | class GO_Presidents_Controller_Party extends GO_Base_Controller_AbstractModelController{ | |
− | + | protected $model = 'GO_Presidents_Model_Party'; | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
} | } | ||
− | |||
− | |||
− | |||
</pre> | </pre> | ||
− | ''' | + | '''PresidentController.php''' |
<pre> | <pre> | ||
<?php | <?php | ||
− | + | class GO_Presidents_Controller_President extends GO_Base_Controller_AbstractModelController { | |
− | + | ||
− | + | protected $model = 'GO_Presidents_Model_President'; | |
− | + | ||
− | + | ||
/** | /** | ||
− | * | + | * Tell the controller to change some column values |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
*/ | */ | ||
− | function | + | protected function formatColumns(GO_Base_Data_ColumnModel $columnModel) { |
− | + | $columnModel->formatColumn('party_id','$model->party->name'); | |
− | $ | + | $columnModel->formatColumn('income_val','$model->income'); |
− | + | return parent::formatColumns($columnModel); | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
} | } | ||
/** | /** | ||
− | * | + | * Display current value in combobox |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
*/ | */ | ||
− | function | + | protected function remoteComboFields(){ |
− | + | return array('party_id'=>'$model->party->name'); | |
− | return | + | |
} | } | ||
+ | } | ||
+ | </pre> | ||
− | + | To the PartyController there is not much to understand. | |
− | + | Extend from <b>GO_Base_Controller_AbstractModelController</b> Tell it what model class it should use and it knows how to Load, Save and Load JSON data for a grid. | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | In the President Controller 2 extra functions are added. | |
− | + | <b>formatColumns()</b> Will make sure that in the datagrid won't show the id, but the name of the party that to president is member of. | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | <b>remoteComboFields()</b> This will make sure that the Combobox is filled with the right data when the President Dialog (Edit form) is shown. | |
− | + | ||
− | + | === Designing the views === | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | Now for the fun part: The client-side of your module. | |
− | + | We'll create the views that display on the screen. This is what our end-users will actually see: | |
+ | We'll need 3 different view: | ||
+ | # a Datagrid for displaying all records | ||
+ | # a Form dialog for editing a record | ||
+ | # a Main panel where we will put the above. | ||
− | + | We already created '''MainPanel.js''' in the modules/views/extjs3/ folder in the beginning of this guide. | |
− | + | Lets modify that file with the following code: | |
− | + | '''MainPanel.js''' | |
− | + | ||
− | + | ||
− | + | ||
− | ''' | + | |
<pre> | <pre> | ||
− | // Creates | + | // Creates namespaces to be used for scoping variables and classes so that they are not global. |
− | Ext.namespace('GO. | + | Ext.namespace('GO.presidents'); |
/* | /* | ||
− | * This | + | * This will add the module to the main tab-panel filled with all the modules |
*/ | */ | ||
− | GO. | + | GO.moduleManager.addModule( |
+ | 'presidents', //Module alias | ||
+ | GO.presidents.PresidentsGrid, //The main panel for this module | ||
+ | { | ||
+ | title : 'Presidents', //Module name in start-menu | ||
+ | iconCls : 'go-module-icon-presidents' //The css class with icon for start-menu | ||
+ | } | ||
+ | ); | ||
+ | </pre> | ||
+ | This file will do no more then search for '''GO.presidents.PresidentsGrid''' to fill the modules MainPanel with. | ||
+ | |||
+ | For our GridPanel we are going to use a Group-Office class that extends Ext.grid.GridPanel. | ||
+ | |||
+ | GO.grid.GridPanel is an extension of the default Ext grid and implements some basic Group-Office functionality like deleting items. | ||
+ | |||
+ | We can now delete selected items in the grid with only the [delete] button. | ||
+ | |||
+ | Lets create this 'PresidentsGrid' in the modules/presidents/views/extjs3/ folder | ||
+ | |||
+ | '''PresidentsGrid.js''' | ||
+ | <pre> | ||
+ | GO.presidents.PresidentsGrid = function(config){ | ||
+ | |||
if(!config) | if(!config) | ||
{ | { | ||
Line 522: | Line 391: | ||
} | } | ||
− | + | //Construct the Gridpanel with the above configuration | |
− | + | GO.presidents.PresidentsGrid.superclass.constructor.call(this, config); | |
− | + | } | |
− | + | ||
− | + | //Extend the PresidentsGrid from GridPanel | |
− | + | Ext.extend(GO.presidents.PresidentsGrid, GO.grid.GridPanel,{ | |
− | + | ||
− | + | }); | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
</pre> | </pre> | ||
− | + | This is the code that is needed for a data-grid but we haven't configured anything yet so it won't do anything for until we do. | |
− | + | For this grid we are going to configure the following properties: | |
− | + | # Some default properties like how to layout and we'll give it a title | |
+ | # Set the Edit dialog class we will use to edit a record. | ||
+ | # A data-store from which the grid needs to pull its data | ||
+ | # A column model, what column we want to display and how. | ||
− | ''' | + | Just before the 'Construct the Gridpanel with the above configuration' comment add the following code to configure what's listed above: |
+ | |||
+ | Some default values for the grid each commented with its use: | ||
<pre> | <pre> | ||
+ | config.title = GO.presidents.lang.presidents; //Title of this panel | ||
+ | config.layout='fit'; //How to lay out the panel | ||
+ | config.autoScroll=true; | ||
+ | config.split=true; | ||
+ | config.autoLoadStore=true; | ||
+ | config.sm=new Ext.grid.RowSelectionModel(); | ||
+ | config.loadMask=true; //Mask the grid when it is loading data | ||
+ | </pre> | ||
− | / | + | Setting the editDialogClass this is rather important. We will create the PresidentDialog later on. |
− | + | Let's first finish configuring this grid. | |
− | + | <pre> | |
− | + | //This dialog will be opened when double clicking a row or calling showEditDialog() | |
− | [{ | + | config.editDialogClass = GO.presidents.PresidentDialog; |
+ | </pre> | ||
+ | |||
+ | Setting the column model of the data-gird. | ||
+ | What column do you want to display and what should be left out? | ||
+ | For all the properties a ColumnModel can have please go to [http://docs.sencha.com/ext-js/3-4/#!/api/Ext.grid.ColumnModel Ext.grid.ColumnModel] | ||
+ | This is just the basic usage. | ||
+ | <pre> | ||
+ | //Configuring the column model | ||
+ | config.cm = new Ext.grid.ColumnModel({ | ||
+ | defaults:{ | ||
+ | sortable: true | ||
+ | }, | ||
+ | columns : [{ | ||
header: '#', | header: '#', | ||
− | |||
dataIndex: 'id', | dataIndex: 'id', | ||
− | + | hidden:true, | |
+ | sortable: false | ||
},{ | },{ | ||
− | header: | + | header: GO.presidents.lang.firstname, |
− | dataIndex: 'firstname' | + | dataIndex: 'firstname' |
− | + | ||
},{ | },{ | ||
− | header: | + | header: GO.presidents.lang.lastname, |
− | dataIndex: 'lastname' | + | dataIndex: 'lastname' |
− | + | ||
},{ | },{ | ||
− | header: | + | header: GO.presidents.lang.party_id, |
− | + | dataIndex: 'party_id' | |
− | dataIndex: ' | + | |
− | + | ||
− | + | ||
},{ | },{ | ||
− | header: | + | header: GO.presidents.lang.tookoffice, |
− | dataIndex: ' | + | dataIndex: 'tookoffice' |
− | + | ||
},{ | },{ | ||
− | header: | + | header: GO.presidents.lang.leftoffice, |
− | dataIndex: ' | + | dataIndex: 'leftoffice' |
− | + | ||
},{ | },{ | ||
− | header: | + | header: GO.presidents.lang.income, |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
dataIndex: 'income', | dataIndex: 'income', | ||
− | + | align: "right" | |
}] | }] | ||
− | ); | + | }); |
− | + | ||
</pre> | </pre> | ||
− | + | As you can see in the above code we created a ColumnModel object and added it to the config | |
+ | For the header value of the columns that will display in our view | ||
− | + | Defining a data-store for the grid: | |
<pre> | <pre> | ||
− | + | //Defining the data store for the grid | |
− | config. | + | config.store = new GO.data.JsonStore({ |
− | + | url: GO.url('presidents/president/store'), | |
− | + | fields: ['id','firstname','lastname','party_id','tookoffice','leftoffice','income'], | |
− | + | remoteSort: true | |
}); | }); | ||
+ | </pre> | ||
− | + | <b>Note:</b> The url: GO.url('presidents/president/store') refers to the methode 'actionStore()' in the President Controller wich it inherits from the AbstractController class. The url will return a JSON string once called. | |
− | + | ||
− | / | + | After setting the data-store we'll add the add the GridView component to the panel: |
− | + | <pre> | |
− | + | //Adding the gridview to the grid panel | |
− | + | config.view=new Ext.grid.GridView({ | |
− | + | emptyText: GO.lang.strNoItemse | |
− | }; | + | }); |
</pre> | </pre> | ||
− | + | In this example we only set the emptyText value this is the text that gets displayed when no records are found by the data store. We are not using a string here but we'll reference to a global language string provided by Group-Office. | |
− | + | For fore info on the Grid view see the [http://docs.sencha.com/ext-js/3-4/#!/api/Ext.grid.GridView Ext.grid.GridView] docs | |
− | + | Our data-grid is now ready for usage. Let's not forget to add our new PresidentsGrid to scripts.txt or else it wont be loaded by the system and or MainPanel.js cant find the Grid | |
− | ''' | + | '''scripts.txt:''' |
<pre> | <pre> | ||
− | / | + | modules/presidents/views/Extjs3/PresidentsGrid.js |
− | + | modules/presidents/views/Extjs3/MainPanel.js | |
− | + | </pre> | |
− | + | ||
− | + | <b>Note:</b> As the PresidentGrid class is used in MainPanel.js the PresidentsGrid.js file needs the be loaded first. | |
− | + | The data-grid should now work and display the presidents of the United States. Note that because of the 'formatColumns()' method we made in the PresidentController not the ID of a party will be displayed in the grid but the name of the party. | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | === Creating a Dialog === | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | Now we are going to create a Dialog which will make editing presidents very easy. | |
− | ''' | + | Create a file modules/president/module/PresidentDialog.js |
+ | |||
+ | '''PresidentDialog.js:''' | ||
<pre> | <pre> | ||
− | + | GO.presidents.PresidentDialog = Ext.extend(GO.dialog.TabbedFormDialog , { | |
− | + | ||
− | + | ||
− | + | initComponent : function(){ | |
+ | |||
+ | Ext.apply(this, { | ||
+ | titleField:'lastname', | ||
+ | goDialogId:'president', | ||
+ | title:GO.presidents.lang.president, | ||
+ | width: 300, | ||
+ | height: 280, | ||
+ | formControllerUrl: 'presidents/president' | ||
+ | }); | ||
+ | |||
+ | GO.presidents.PresidentDialog.superclass.initComponent.call(this); | ||
+ | } | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
}); | }); | ||
+ | </pre> | ||
− | + | This will create an empty dialog that will open when we double click a row in the grid. But it doesn't work yet. This is because we haven't loaded the PresidentDialog.js file. Lets do this by changing your 'scripts.txt' file again. | |
− | + | ||
− | + | ||
− | + | '''scripts.txt:''' | |
+ | <pre> | ||
+ | modules/presidents/views/Extjs3/PresidentsGrid.js | ||
+ | modules/presidents/views/Extjs3/PresidentDialog.js | ||
+ | modules/presidents/views/Extjs3/MainPanel.js | ||
</pre> | </pre> | ||
+ | === Adding some language strings === | ||
− | + | In the PresidentDialog.js file you can see that the <b>title</b> property of the dialog is set to <b>GO.presidents.lang.president</b> This language string doesn't exists yet. So let's add some translatable strings that we can use in our PresidentDialog. | |
− | + | ||
− | + | Open up modules/language/en.php and write the following: | |
− | ''' | + | '''en.php:''' |
<pre> | <pre> | ||
− | // | + | $l = array( |
− | + | 'name' => ' Presidents', //this name will appear in the Start menu -> modules section | |
+ | 'description' => 'A simple test module', //this description will appear in the Start menu -> modules section | ||
− | + | 'presidents' => 'Presidents', | |
− | + | 'president' => 'President', | |
− | + | 'party_id' => 'Party', | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | 'tookoffice' => 'Entering Office', | |
− | + | 'leftoffice' => 'Leaving Office', | |
− | + | 'income' => 'Income', | |
− | + | 'firstname' => 'First name', | |
− | + | 'lastname' => 'Last name', | |
− | + | ); | |
− | + | ||
− | + | ||
</pre> | </pre> | ||
− | + | <b>GO.presidents.lang.president</b> in the title of our PresidentDialog will now display: "President" | |
+ | The other language strings you see will be used for the form that will be created inside the dialog. | ||
− | + | === Creating a form inside the dialog === | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | In the PresidentDialog.js file we have created a PresidentDialog extended from TabbedFormDialog and overwritten the function initComponents and set some properties there. | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | We are going to overwrite one more method in the PresidentDialog. | |
+ | Add a comma (,) at the end of the initComponent function and add the following code afterwards: | ||
'''PresidentDialog.js:''' | '''PresidentDialog.js:''' | ||
− | <pre> | + | <pre> |
− | + | buildForm : function () { | |
− | + | ||
− | this. | + | this.propertiesPanel = new Ext.Panel({ |
− | + | title:GO.lang.strProperties, | |
− | cls:'go-form-panel' | + | cls:'go-form-panel', |
layout:'form', | layout:'form', | ||
− | + | items:[{ | |
− | items: [{ | + | xtype: 'textfield', |
name: 'firstname', | name: 'firstname', | ||
− | + | width:300, | |
− | + | ||
anchor: '100%', | anchor: '100%', | ||
+ | maxLength: 100, | ||
allowBlank:false, | allowBlank:false, | ||
− | + | fieldLabel: GO.presidents.lang.firstname | |
− | },{ | + | }, |
+ | { | ||
+ | xtype: 'textfield', | ||
name: 'lastname', | name: 'lastname', | ||
− | + | fieldLabel: GO.presidents.lang.lastname, | |
− | fieldLabel: | + | anchor: '100%' |
− | anchor: '100%', | + | }, |
− | + | { | |
− | + | xtype:'combo', | |
− | + | fieldLabel: GO.presidents.lang.party_id, | |
− | hiddenName:' | + | hiddenName:'party_id', |
− | + | anchor:'100%', | |
− | + | emptyText:GO.lang.strPleaseSelect, | |
store: new GO.data.JsonStore({ | store: new GO.data.JsonStore({ | ||
− | url: GO | + | url: GO.url('presidents/party/store'), |
− | baseParams: { | + | baseParams: { |
− | + | permissionLevel:GO.permissionLevels.write | |
− | } | + | }, |
− | + | fields: ['id', 'name'] | |
− | + | ||
− | + | ||
− | fields: [' | + | |
− | + | ||
}), | }), | ||
− | valueField:' | + | valueField:'id', |
− | displayField:' | + | displayField:'name', |
− | + | ||
triggerAction: 'all', | triggerAction: 'all', | ||
+ | editable: true, | ||
forceSelection: true, | forceSelection: true, | ||
− | |||
allowBlank: false | allowBlank: false | ||
− | } | + | }, |
+ | { | ||
+ | xtype: 'datefield', | ||
name: 'tookoffice', | name: 'tookoffice', | ||
− | fieldLabel | + | fieldLabel: GO.presidents.lang.tookoffice, |
− | + | ||
allowBlank: false, | allowBlank: false, | ||
− | anchor:'100%' | + | anchor: '100%' |
− | } | + | }, |
+ | { | ||
+ | xtype: 'datefield', | ||
name: 'leftoffice', | name: 'leftoffice', | ||
− | fieldLabel | + | fieldLabel: GO.presidents.lang.leftoffice, |
− | + | ||
allowBlank: false, | allowBlank: false, | ||
anchor: '100%' | anchor: '100%' | ||
− | } | + | }, |
+ | { | ||
+ | xtype: 'numberfield', | ||
name: 'income', | name: 'income', | ||
− | fieldLabel: | + | value: GO.util.numberFormat(0), |
+ | fieldLabel: GO.presidents.lang.income, | ||
allowBlank: false, | allowBlank: false, | ||
− | + | anchor: '100%' | |
− | + | }] | |
− | } | + | |
}); | }); | ||
− | + | this.addPanel(this.propertiesPanel); | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
} | } | ||
− | |||
</pre> | </pre> | ||
− | + | This will add a panel to our PresidentDialog and add 6 form-fields inside. | |
+ | The combo/dropdown field is a bit tricky. | ||
− | + | It's needs to know where to pull the data that it is going to list from. | |
+ | for this we set the store property of the field and fill it with a <b>JsonStore</b> object. This object needs to know a controller action that returns JSON data. We'll refer to the 'store' action in the PartyController we've created earlier. Also we specify the fields the be used for the key and value of the dropdown menu. In this case 'id' and 'name'. | ||
− | + | When double clicking a row the President dialog will now show a form with the above defined fields. | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
In the next section we are going to add a toolbar which will allow us to create new presidents. | In the next section we are going to add a toolbar which will allow us to create new presidents. | ||
− | |||
=== Adding a Toolbar === | === Adding a Toolbar === | ||
Line 953: | Line 662: | ||
We are now going to add a toolbar in the top with links to create or delete a president. | We are now going to add a toolbar in the top with links to create or delete a president. | ||
− | All of the following code has to be added within the | + | All of the following code has to be added within the PresidentsGrid configuration. |
− | ''' | + | '''PresidentsGrid.js:''' |
<pre> | <pre> | ||
− | + | //Setup a toolbar for the grid panel | |
− | + | config.tbar = new Ext.Toolbar({ | |
− | + | cls:'go-head-tb', | |
− | + | items: [ | |
− | + | { | |
− | tbar | + | iconCls: 'btn-add', |
− | + | text: GO.lang['cmdAdd'], | |
− | + | cls: 'x-btn-text-icon', | |
− | + | handler: function(){ | |
− | + | this.showEditDialog(); | |
− | + | }, | |
− | + | scope: this | |
− | + | }, | |
− | + | { | |
− | + | iconCls: 'btn-delete', | |
− | + | text: GO.lang['cmdDelete'], | |
− | + | cls: 'x-btn-text-icon', | |
− | + | handler: function(){ | |
− | + | this.deleteSelected(); | |
− | + | }, | |
− | + | scope: this | |
− | + | }, | |
− | + | GO.lang.strSearch + ':', | |
− | + | new GO.form.SearchField({ | |
− | + | store: config.store, | |
− | + | width:320 | |
− | + | }) | |
− | + | ] | |
− | + | }); | |
− | }); | + | |
</pre> | </pre> | ||
− | + | Setting a <b>Ext.Toolbar</b> object to the tbar property of the GridPanel will render a toolbar on top of the grid. In this example we'll add 4 items to the toolbar: | |
− | ' | + | # An add button that will call the PresidentDialog but this time it's empty and can be used to add a new record |
− | + | # A delete button that will delete the selected row. | |
− | + | # A string | |
− | + | # A search Field. | |
− | + | ||
− | + | For more information about the see the [http://docs.sencha.com/ext-js/3-4/#!/api/Ext.Toolbar Ext.Toolbar] docs | |
− | + | ||
Line 1,004: | Line 711: | ||
This one is very easy to do because we used a GO.grid.GridPanel instead of the Ext one. | This one is very easy to do because we used a GO.grid.GridPanel instead of the Ext one. | ||
− | We only need to add 1 line to our PresidentsGrid | + | We only need to add 1 line to our PresidentsGrid configuration and our PHP takes care of the rest. |
− | PresidentsGrid.js: | + | '''PresidentsGrid.js:''' |
<pre> | <pre> | ||
/* config.paging is an added functionality by Group-Office | /* config.paging is an added functionality by Group-Office | ||
Line 1,013: | Line 720: | ||
config.paging=true, | config.paging=true, | ||
</pre> | </pre> | ||
− | |||
=== Adding some style === | === Adding some style === | ||
Line 1,021: | Line 727: | ||
Create Directories: | Create Directories: | ||
<pre> | <pre> | ||
− | modules/ | + | modules/presidents/themes/ |
− | modules/ | + | modules/presidents/themes/Default/ |
</pre> | </pre> | ||
− | Now we create the file modules/ | + | Now we create the file modules/presidents/themes/Default/style.css |
Most modules will want to have it's own module-icon, so we add this default rule for later use. | Most modules will want to have it's own module-icon, so we add this default rule for later use. | ||
Line 1,032: | Line 738: | ||
<pre> | <pre> | ||
/* This class is used to show the icon in the main tabpanel of GO */ | /* This class is used to show the icon in the main tabpanel of GO */ | ||
− | .go-module-icon- | + | .go-module-icon-presidents { |
} | } | ||
</pre> | </pre> | ||
− | In our Grid we can't change the ID's of presidents. To make it apparent that this field is treated differently we will add a renderer that sets the cell | + | In our Grid we can't change the ID's of presidents. To make it apparent that this field is treated differently we will add a renderer that sets the cell CSS style so it will use a different background. |
− | To do this we need to change the corresponding column config object in our | + | To do this we need to change the corresponding column config object in our PresidentsGrid ColumnModel. |
'''PresidentsGrid.js:''' | '''PresidentsGrid.js:''' | ||
<pre> | <pre> | ||
− | header: '#', | + | { |
− | readOnly: true, | + | header: '#', |
− | dataIndex: 'id', | + | readOnly: true, |
− | renderer: function(value, cell){ | + | dataIndex: 'id', |
− | + | renderer: function(value, cell){ | |
− | + | cell.css = "readonlycell"; | |
− | }, | + | return value; |
− | width: 50 | + | }, |
+ | width: 50 | ||
+ | } | ||
</pre> | </pre> | ||
Line 1,065: | Line 773: | ||
'''PresidentsGrid.js:''' | '''PresidentsGrid.js:''' | ||
<pre> | <pre> | ||
− | header: "Income", | + | { |
− | dataIndex: 'income', | + | header: "Income", |
− | width: 120, | + | dataIndex: 'income', |
− | renderer: function(value, | + | width: 120, |
− | + | renderer: function(value, metaData, record){ | |
− | + | if(record.data.income_val > 1000000){ | |
− | + | metaData.attr = 'style="color:#336600;"'; | |
− | + | } else if (record.data.income_val > 100000){ | |
− | + | metaData.attr = 'style="color:#FF9900;"'; | |
− | + | } else { | |
− | + | metaData.attr = 'style="color:#CC0000;"'; | |
− | } | + | } |
− | + | return "$ "+value; | |
+ | }, | ||
+ | align: "right" | ||
} | } | ||
</pre> | </pre> | ||
Line 1,084: | Line 794: | ||
We now have a fully functional module which lets us add, update and delete presidents. | We now have a fully functional module which lets us add, update and delete presidents. | ||
− | |||
==Putting custom fields in module items== | ==Putting custom fields in module items== | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | This will only work with the professional version and you must have the Custom fields module installed. To create custom fields for a model, we must create a custom field module. We'll place it in the following directory: | |
− | + | modules/presidents/customfields/model/ | |
− | + | ||
+ | '''President.php''' | ||
<pre> | <pre> | ||
− | + | class GO_Presidents_Customfields_Model_President extends GO_Customfields_Model_AbstractCustomFieldsRecord{ | |
− | { | + | /** |
− | + | * Returns a static model of itself | |
− | { | + | * |
− | + | * @param String $className | |
+ | * @return GO_Presidents_Model_CustomFieldsRecord | ||
+ | */ | ||
+ | public static function model($className=__CLASS__) | ||
+ | { | ||
+ | return parent::model($className); | ||
+ | } | ||
+ | |||
+ | public function extendsModel(){ | ||
+ | return "GO_Presidents_Model_President"; | ||
} | } | ||
} | } | ||
</pre> | </pre> | ||
− | + | Our President model still needs to know what the classname of the CustomerFields model is. For this we'll add the Following method the our President <b>Model</b>: | |
+ | '''President.php''' | ||
<pre> | <pre> | ||
− | + | public function customfieldsModel(){ | |
− | { | + | return "GO_Presidents_Customfields_Model_President"; |
− | + | ||
} | } | ||
</pre> | </pre> | ||
− | + | Also the customer fields we specify need to be saved into the database For this we'll create a table named 'cf_pm_presidents': | |
− | + | <pre> | |
− | + | CREATE TABLE IF NOT EXISTS `cf_pm_presidents` ( | |
+ | `model_id` int(11) NOT NULL DEFAULT '0', | ||
+ | PRIMARY KEY (`model_id`) | ||
+ | ); | ||
− | + | INSERT INTO `cf_pm_presidents` (`model_id`) VALUES (1); | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
</pre> | </pre> | ||
− | + | <b>Note:</b> The table name needs to be the same as the table name where the records are saved but prefixed with ' cf_' | |
− | + | This is all there is to add custom field functionality. After this we still need to change our views to display the input fields in our edit dialog. Add the following line just before the <b>initComponent</b> function | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | </ | + | |
− | + | '''PresidentDialog.php''' | |
<pre> | <pre> | ||
− | + | customFieldType : "GO_Presidents_Model_President", | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
</pre> | </pre> | ||
− | + | This should make the customfields work all we have to do is create some fields. | |
− | + | Install the <b>Custom fields</b> module if you haven't done this already. This module is only available in the pro version of Group-Office. | |
+ | When it's installed it will show up in the Admin menu of the start-menu. Start it and in the left sidebar you will see the the President Model is there. Click it and add a Category. Name it: 'Appearance'. | ||
− | + | We'll add the following field to this category: | |
− | + | Name: Hair color, Type: Text | |
− | + | Name: Birthday, Type: Date | |
− | + | Name: Glasses, Type: Checkbox | |
− | + | ||
+ | To check if the fields will work. We first need to reload Group Office. Refresh the page, go to the Presidents module and edit a record. See the extra tab Appearance in to PresidentDialog? | ||
+ | |||
+ | It's also possible to display customfields in the data-grid if you like. The reason that the fields aren't displaying in the grid yet is because the DataStore of the grid doesn't know what the model type is. It only has attributes. | ||
+ | |||
+ | Lets change to config.store attribute of the configuration of our PresidentGrid | ||
+ | '''PresidentGrid.js:''' | ||
+ | <pre> | ||
+ | //Defining the data store for the grid | ||
+ | config.store = new GO.data.JsonStore({ | ||
+ | url: GO.url('presidents/president/store'), | ||
+ | fields: ['id','firstname','lastname','party_id','tookoffice','leftoffice','income', 'income_val'], | ||
+ | remoteSort: true, | ||
+ | model: 'GO_Presidents_Model_President' | ||
+ | }); | ||
</pre> | </pre> | ||
+ | All your defines customfields will be visible into the data-grid. | ||
+ | <b>Note:</b> The following fieldtypes will never be rendered in the grid: 'textarea','html', 'heading', 'infotext' | ||
==Making your items linkable== | ==Making your items linkable== | ||
− | + | In Group-Office we can link one item to another. If your newly create module has an item that should be linkable we add the following overwrite the hasLinks() method in our model | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | We are going to make the presidents linkable, for this we add the following methods to our President Model: | |
+ | '''President.php''' | ||
+ | <pre> | ||
+ | public function hasLinks() { | ||
+ | return true; | ||
+ | } | ||
− | + | public function getFullname() { | |
− | + | return $this->firstname . " " . $this->lastname; | |
+ | } | ||
− | + | protected function getCacheAttributes() { | |
+ | return array("name"=>$this->fullname, "description"=>$this->party->name); | ||
+ | } | ||
+ | </pre> | ||
− | + | Next our database needs a table where to links for the presidents will be saved. We execute the following query on our database: | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | '''SQL''' | |
+ | <pre> | ||
+ | CREATE TABLE IF NOT EXISTS `go_links_pm_presidents` ( | ||
+ | `id` int(11) NOT NULL, | ||
+ | `folder_id` int(11) NOT NULL, | ||
+ | `model_id` int(11) NOT NULL, | ||
+ | `model_type_id` int(11) NOT NULL, | ||
+ | `description` varchar(100) DEFAULT NULL, | ||
+ | `ctime` int(11) NOT NULL, | ||
+ | KEY `link_id` (`model_id`,`model_type_id`), | ||
+ | KEY `id` (`id`,`folder_id`), | ||
+ | KEY `ctime` (`ctime`) | ||
+ | ) ENGINE=MyISAM DEFAULT CHARSET=utf8; | ||
+ | </pre> | ||
+ | <b>Note:</b> the table name is the same as where the president records are stored but prefixed with 'go_links_' | ||
+ | |||
+ | In the PresidentDialog we will add a field to add links to the record after the income numberfield we'll add a new Go.form.SelectLink componentent. To code will look like this: | ||
+ | |||
+ | '''PresidentDialog.js''' | ||
<pre> | <pre> | ||
− | + | { | |
− | + | xtype: 'numberfield', | |
− | + | name: 'income', | |
− | + | value: GO.util.numberFormat(0), | |
+ | fieldLabel: GO.presidents.lang.income, | ||
+ | allowBlank: false, | ||
+ | anchor: '100%' | ||
+ | }, | ||
+ | new GO.form.SelectLink({ | ||
+ | anchor:'100%' | ||
+ | }) | ||
</pre> | </pre> | ||
− | + | This is all what's needed for adding other linkable Group-Office record to the president record. But where are they? Let's make a panel to display the links. We'll place this on the right side of the grid. | |
− | + | The MainPanel will be seperated into 2 divided panels. Our PresidentGrid in the middle and the (still to build) PresidentPanel on the right. Let's build the PresidentPanel | |
− | < | + | '''PresidentPanel.js''' |
+ | <pre> | ||
+ | GO.presidents.PresidentPanel = Ext.extend(GO.DisplayPanel,{ | ||
+ | model_name : "GO_Presidents_Model_President", | ||
+ | noFileBrowser : true, | ||
+ | stateId : 'pm-president-panel', | ||
+ | |||
+ | editHandler : function(){ | ||
+ | if(!GO.presidents.presidentDialog) | ||
+ | GO.presidents.presidentDialog = new GO.presidents.PresidentDialog(); | ||
+ | GO.presidents.presidentDialog.show(this.link_id); | ||
+ | }, | ||
+ | |||
+ | initComponent : function(){ | ||
+ | |||
+ | this.loadUrl=GO.url('presidents/president/display'); | ||
+ | |||
+ | this.template = | ||
+ | '<table class="display-panel" cellpadding="0" cellspacing="0" border="0">'+ | ||
+ | '<tr>'+ | ||
+ | '<td colspan="2" class="display-panel-heading">{firstname} {lastname}</td>'+ | ||
+ | '</tr>'+ | ||
+ | '<tr>'+ | ||
+ | '<td>ID:</td>'+ | ||
+ | '<td>{id}</td>'+ | ||
+ | '</tr>'+ | ||
+ | '<tr>'+ | ||
+ | '<td>'+GO.presidents.lang.party_id+':</td>'+ | ||
+ | '<td>{partyName}</td>'+ | ||
+ | '</tr>'+ | ||
+ | '<tr>'+ | ||
+ | '<td>'+GO.presidents.lang.tookoffice+':</td>'+ | ||
+ | '<td>{tookoffice}</td>'+ | ||
+ | '</tr>'+ | ||
+ | '<tr>'+ | ||
+ | '<td>'+GO.presidents.lang.leftoffice+':</td>'+ | ||
+ | '<td>{leftoffice}</td>'+ | ||
+ | '</tr>'+ | ||
+ | '<tr>'+ | ||
+ | '<td>'+GO.presidents.lang.income+':</td>'+ | ||
+ | '<td>{income}</td>'+ | ||
+ | '</tr>'+ | ||
+ | '</table>'; | ||
+ | |||
+ | if(GO.customfields) | ||
+ | { | ||
+ | this.template +=GO.customfields.displayPanelTemplate; | ||
+ | } | ||
+ | if(GO.tasks) | ||
+ | this.template +=GO.tasks.TaskTemplate; | ||
− | = | + | if(GO.calendar) |
− | + | this.template += GO.calendar.EventTemplate; | |
+ | |||
+ | if(GO.workflow) | ||
+ | this.template +=GO.workflow.WorkflowTemplate; | ||
− | + | this.template += GO.linksTemplate; | |
+ | |||
+ | Ext.apply(this.templateConfig, GO.linksTemplateConfig); | ||
+ | |||
+ | if(GO.comments) | ||
+ | { | ||
+ | this.template += GO.comments.displayPanelTemplate; | ||
+ | } | ||
− | + | GO.presidents.PresidentPanel.superclass.initComponent.call(this); | |
+ | } | ||
+ | }); | ||
+ | </pre> | ||
− | + | After creating this Panel we still have to load it when our module loads. Add the following line on top of the scripts.txt file in your view folder. | |
− | + | '''scripts.txt''' | |
+ | <pre> | ||
+ | modules/presidents/views/Extjs3/PresidentPanel.js | ||
+ | </pre> | ||
− | + | Lets make a split panel. where the center is our grid listing presidents. en the right panel will display the presidents details and links. Change the code in MainPanel.js to the following: | |
+ | '''MainPanel.js''' | ||
<pre> | <pre> | ||
− | + | // Creates namespaces to be used for scoping variables and classes so that they are not global. | |
− | + | Ext.namespace('GO.presidents'); | |
− | + | ||
− | + | ||
− | + | GO.presidents.MainPanel = function(config){ | |
− | + | ||
+ | if(!config) | ||
+ | { | ||
+ | config = {}; | ||
+ | } | ||
+ | |||
+ | this.centerPanel = new GO.presidents.PresidentsGrid({ | ||
+ | region:'center', | ||
+ | id:'pm-center-panel', | ||
+ | border:true | ||
+ | }); | ||
+ | |||
+ | this.centerPanel.on("delayedrowselect",function(grid, rowIndex, r){ | ||
+ | this.eastPanel.load(r.data.id); | ||
+ | }, this); | ||
− | + | this.centerPanel.store.on('load', function(){ | |
− | // | + | this.eastPanel.reset(); |
+ | }, this); | ||
+ | |||
+ | this.eastPanel = new GO.presidents.PresidentPanel({ | ||
+ | region:'east', | ||
+ | id:'pm-east-panel', | ||
+ | width:440, | ||
+ | border:true | ||
+ | }); | ||
+ | |||
+ | |||
+ | //Setup a toolbar for the grid panel | ||
+ | config.tbar = new Ext.Toolbar({ | ||
+ | cls:'go-head-tb', | ||
+ | items: [ | ||
+ | { | ||
+ | iconCls: 'btn-add', | ||
+ | text: GO.lang['cmdAdd'], | ||
+ | cls: 'x-btn-text-icon', | ||
+ | handler: function(){ | ||
+ | this.centerPanel.showEditDialog(); | ||
+ | }, | ||
+ | scope: this | ||
+ | }, | ||
+ | { | ||
+ | iconCls: 'btn-delete', | ||
+ | text: GO.lang['cmdDelete'], | ||
+ | cls: 'x-btn-text-icon', | ||
+ | handler: function(){ | ||
+ | this.centerPanel.deleteSelected(); | ||
+ | }, | ||
+ | scope: this | ||
+ | } | ||
+ | ] | ||
+ | }); | ||
+ | |||
+ | config.items=[ | ||
+ | this.centerPanel, | ||
+ | this.eastPanel | ||
+ | ]; | ||
+ | |||
+ | config.layout='border'; | ||
+ | GO.presidents.MainPanel.superclass.constructor.call(this, config); | ||
+ | }; | ||
+ | |||
+ | Ext.extend(GO.presidents.MainPanel, Ext.Panel, {}); | ||
/* | /* | ||
− | * | + | * This will add the module to the main tabpanel filled with all the modules |
− | + | ||
− | + | ||
− | + | ||
*/ | */ | ||
+ | GO.moduleManager.addModule( | ||
+ | 'presidents', //Module alias | ||
+ | GO.presidents.MainPanel, //The main panel for this module | ||
+ | { | ||
+ | title : 'Presidents', //Module name in startmenu | ||
+ | iconCls : 'go-module-icon-presidents' //The css class with icon for startmenu | ||
+ | } | ||
+ | ); | ||
− | + | /* | |
− | + | * Add linkHandeler for linking models from this module to other GroupOffice models | |
− | + | */ | |
− | + | GO.linkHandlers["GO_Presidents_Model_President"]=function(id){ | |
− | + | if(!GO.presidents.linkWindow){ | |
− | + | var presidentPanel = new GO.presidents.PresidentPanel(); | |
− | + | GO.presidents.linkWindow= new GO.LinkViewWindow({ | |
− | + | title: GO.presidents.lang.president, | |
− | + | items: presidentPanel, | |
− | + | presidentPanel: presidentPanel, | |
− | + | closeAction:"hide" | |
+ | }); | ||
+ | } | ||
+ | GO.presidents.linkWindow.presidentPanel.load(id); | ||
+ | GO.presidents.linkWindow.show(); | ||
+ | return GO.presidents.linkWindow; | ||
+ | } | ||
+ | </pre> | ||
− | + | As you can see in the New MainPanel.js view this panel has a 'config.tbar' defines with the 2 'add' and 'delete' buttons. Note that the reference to the showEditDialog() method has changed to 'this.centerPanel' instead of 'this' because the centerPanel now represents the PresidentsGrid. | |
− | + | So this buttons can be removed from the toolbar in the PresidentGrid.js | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | So change to config.tbar option in the PresidentGrid.js to the following. | |
− | + | '''PresidentGrid.js:''' | |
+ | <pre> | ||
+ | config.tbar = new Ext.Toolbar({ | ||
+ | items: [ | ||
+ | GO.lang.strSearch + ':', | ||
+ | new GO.form.SearchField({ | ||
+ | store: config.store, | ||
+ | width:320 | ||
+ | }) | ||
+ | ] | ||
+ | }) | ||
+ | </pre> | ||
− | + | This way only the searchfield will remain in the toolbar of the PresidentsGrid. The Add and Delete button are already placed on the MainPanel's toolbar | |
+ | == Extending the defaults == | ||
− | + | Do you see the Add and Edit button in top of your panel being grayed out. The main reason for this it because the edit panel has build in functionality the check if you have permission the read or write a record. this way you can set this permission per record. | |
− | + | For our presidents example it's not very logic to have these permission per record. We didn't specify this in the database and are not going to use it in this example. for this reason. we need to overwrite the data that we get from the server. | |
+ | Lets have o look at what's returned: | ||
<pre> | <pre> | ||
− | + | {"data": | |
− | + | {"id":"40", | |
+ | "party_id":"6", | ||
+ | "firstname":"Ronald", | ||
+ | "lastname":"Reagan", | ||
+ | "tookoffice":"20-01-1981", | ||
+ | "leftoffice":"20-01-1989", | ||
+ | "income":"99,867,297.35", | ||
+ | "model":"GO_Presidents_Model_President", | ||
+ | "permission_level":4, | ||
+ | "write_permission":true, | ||
+ | "customfields":[{ | ||
+ | "id":"1", | ||
+ | "name":"Appearance", | ||
+ | "fields":[{ | ||
+ | "name":"Glasses", | ||
+ | "value":"No" | ||
+ | }] | ||
+ | }], | ||
+ | "links":[], | ||
+ | "events":[], | ||
+ | "tasks":[], | ||
+ | "files":[], | ||
+ | "comments":[] | ||
+ | }, | ||
+ | "success":true} | ||
+ | </pre> | ||
− | + | All display view in Group-Office are ExtJS components or Group-Office components that are extended from ExtJS components. All of these components are filled with JSON data. This data is loaded Asynchrone with Javascript. Let look at what data will be loaded | |
− | + | ||
− | + | Let's modify the JSON data that is returned. Add the following function to the PresidentController | |
− | + | ||
− | $ | + | '''PresidentController.php:''' |
− | / | + | <pre> |
+ | protected function afterDisplay(&$response, &$model, &$params) | ||
+ | { | ||
+ | $response['data']['partyName'] = $model->party->name; | ||
+ | return parent::beforeDisplay($response, $model, $params); | ||
+ | } | ||
+ | </pre> | ||
+ | With this the JSON data that is returned in actionDisplay() will be overwritten. a field partyName will be added to our returned JSON data. this will be used to display the party name in the PresidentPanel | ||
− | + | == Installation and upgrades == | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | $ | + | Files in the modules/presidents/install folder are important for installing, uninstalling and upgrading modules. |
+ | install.sql and uninstall.sql are automatically applied to the database when installing or uninstalling. | ||
+ | updates.php is used for upgrading. | ||
+ | |||
+ | It looks like this for example: | ||
+ | |||
+ | <pre> | ||
+ | <?php | ||
+ | $updates["201108131011"][]="ALTER TABLE `ab_companies` CHANGE `id` `id` INT( 11 ) NOT NULL AUTO_INCREMENT"; | ||
+ | $updates["201108131011"][]="ALTER TABLE `ab_contacts` CHANGE `id` `id` INT( 11 ) NOT NULL AUTO_INCREMENT"; | ||
+ | $updates["201108131011"][]="ALTER TABLE `ab_contacts` ADD `go_user_id` INT NOT NULL , ADD INDEX ( `go_user_id` )"; | ||
+ | $updates["201204270920"][]="script:1_some_script.php"; | ||
</pre> | </pre> | ||
− | + | The timestamp is in this format: | |
+ | |||
+ | "YYYYMMDDHHMM" | ||
+ | |||
+ | The timestamp is important because all updates are executed chronologically. Because modules can depend on each others updates. | ||
− | + | You can also use scripts. It's good practice to avoid using update scripts as much as possible. When scripts use functions of the framework and they change later they might break. | |
+ | The entry "script:1_some_script.php" will include modules/[moduleid]/install/updatescripts/1_some_script.php. | ||
− | + | <b>Note:</b> When putting update queries in the updates.php file, don't forget to update install.sql too. |
Latest revision as of 15:04, 10 June 2014
Contents
Creating an empty module
Now we will create the main module class.
A Group-Office module will extend the GO_Base_Module class.
Create the file module/presidents/PresidentsModule.php
PresidentsModule.php:
class GO_Presidents_PresidentsModule extends GO_Base_Module{ public function autoInstall() { return true; } public function author() { return 'Merijn Schering'; } public function authorEmail() { return 'mschering@intermesh.nl'; } }
Creating our first view
Now we will create the main panel of the module. All views in Group-Office view is an extension of an ExtJS Panel. So it can be put in any other ExtJS Container.
For the main panel create the file: module/presidents/views/Extjs3/MainPanel.js
MainPanel.js:
// Creates namespaces to be used for scoping variables and classes so that they are not global. Ext.namespace('GO.presidents'); /* * This is the constructor of our MainPanel */ GO.presidents.MainPanel = function(config){ if(!config) { config = {}; } config.html='Hello World'; /* * Explicitly call the superclass constructor */ GO.presidents.MainPanel.superclass.constructor.call(this, config); } /* * Extend our MainPanel from the ExtJS Panel */ Ext.extend(GO.presidents.MainPanel, Ext.Panel,{ }); /* * This will add the module to the main tab-panel filled with all the modules */ GO.moduleManager.addModule( 'presidents', //Module alias GO.presidents.MainPanel, //The main panel for this module { title : 'Presidents', //Module name in start-menu iconCls : 'go-module-icon-presidents' //The css class with icon for start-menu } );
This view will only render the text 'Hello World'
For the main panel to be loaded when the module is active, we'll need to create a file called scripts.txt in our /modules/presidents/views/ExtJs3/ folder.
scripts.txt:
modules/presidents/views/Extjs3/MainPanel.js
Without this file your module will never work!
Multilingual functions
To make the module translatable create the follow file: modules/presidents/language/en.php:
en.php
<?php /** * This $l array excists of key-value pairs * key is what should be called in the view eg. GO.presidents.name * value is the string that will be displayed in your view */ $l = array( 'name' => ' Presidents', //this name will appear in the Start menu -> modules section 'description' => 'A simple test module', //this description will appear in the Start menu -> modules section );
These two string are the name and the description of the Group-Office module and are use by the module installation dialog.
After completing these steps you should be able to install your module and be able to display the hello world view that was created.
In Group-Office go to 'Admin menu->modules' and install your newly created module.
The Tutorial
For this tutorial we will assume that you finished the first sections on this page.
The example module we are going to make will show you how to create a simple module that displays data from a database and lets us add items to it, change them and delete them.
All files are also available in the Group-Office download. You can find them in modules/presidents/.
Before we start
First, we need to create the database containing the data we are going to work with.
We will create 2 tables named 'pm_parties' and 'pm_presidents' It is always a good idea to prefix the database tables used for a module (pm_ is short for President Module)
Run the following MySQL query on the Group-Office database of your installation:
CREATE TABLE IF NOT EXISTS `pm_parties` ( `id` int(11) NOT NULL auto_increment, `name` varchar(40) NOT NULL, PRIMARY KEY (`id`) ); INSERT INTO `pm_parties` (`id`, `name`) VALUES (1, 'No Party'), (2, 'Federalist'), (3, 'Democratic-Republican'), (4, 'Democratic'), (5, 'Whig'), (6, 'Republican'); CREATE TABLE IF NOT EXISTS `pm_presidents` ( `id` int(11) NOT NULL auto_increment, `party_id` int(11) NOT NULL, `firstname` varchar(20) NOT NULL, `lastname` varchar(20) NOT NULL, `tookoffice` date NOT NULL, `leftoffice` date NOT NULL, `income` decimal(14,2) NOT NULL, `ctime` int(11) NOT NULL DEFAULT '0', `mtime` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ); INSERT INTO `pm_presidents` (`id`, `party_id`, `firstname`, `lastname`, `tookoffice`, `leftoffice`, `income`) VALUES (1, 1, 'George', 'Washington', '1789-04-30', '1797-03-04', 135246.32), (2, 2, 'John', 'Adams', '1797-03-04', '1801-03-04', 236453.34), (3, 3, 'Thomas', 'Jefferson', '1801-03-04', '1809-03-04', 468043.25), (4, 3, 'James', 'Madison', '1809-03-04', '1817-03-04', 649273.00), (5, 3, 'James', 'Monroe', '1817-03-04', '1825-03-04', 374937.23), (6, 3, 'John', 'Quincy Adams', '1825-03-04', '1829-03-04', 649824.35), (7, 4, 'Andrew', 'Jackson', '1829-03-04', '1837-03-04', 3972753.12), (8, 4, 'Martin', 'Van Buren', '1837-03-04', '1841-03-04', 325973.24), (9, 5, 'William', 'Harrison', '1841-03-04', '1841-04-04', 25532.08), (10, 5, 'John', 'Tyler', '1841-04-04', '1845-03-04', 235542.35), (11, 4, 'James', 'Polk', '1845-03-04', '1849-03-04', 3264972.35), (12, 5, 'Zachary', 'Taylor', '1849-03-04', '1850-07-09', 35974.35), (13, 5, 'Millard', 'Fillmore', '1850-07-09', '1853-03-04', 35792.45), (14, 4, 'Franklin', 'Pierce', '1853-03-04', '1857-03-04', 357938.35), (15, 4, 'James', 'Buchanan', '1857-03-04', '1861-03-04', 357937.35), (16, 6, 'Abraham', 'Lincoln', '1861-03-04', '1865-04-15', 25073.40), (17, 4, 'Andrew', 'Johnson', '1865-04-15', '1869-03-04', 356279.35), (18, 6, 'Ulysses', 'Grant', '1869-03-04', '1877-03-04', 357938.35), (19, 6, 'Rutherford', 'Hayes', '1877-03-04', '1881-03-04', 2359737.35), (20, 6, 'James', 'Garfield', '1881-03-04', '1881-09-19', 3579.24), (21, 6, 'Chester', 'Arthur', '1881-09-19', '1995-03-04', 357932.35), (22, 4, 'Grover', 'Cleveland', '1885-03-04', '1889-03-04', 369723.35), (23, 6, 'Benjamin', 'Harrison', '1889-03-04', '1893-03-04', 357392.35), (24, 4, 'Grover', 'Cleveland', '1893-03-04', '1897-03-04', 3275935.35), (25, 6, 'William', 'McKinley', '1897-03-04', '1901-09-14', 35793.35), (26, 6, 'Theodore', 'Roosevelt', '1901-09-14', '1909-03-04', 0.00), (27, 6, 'William', 'Taft', '1909-03-04', '1913-03-04', 239597.35), (28, 4, 'Woodrow', 'Wilson', '1913-03-04', '1921-03-04', 32579743.34), (29, 6, 'Warren', 'Harding', '1921-03-04', '1923-08-02', 35793.24), (30, 6, 'Calvin', 'Coolidge', '1923-08-02', '1929-03-04', 296529.24), (31, 6, 'Herbert', 'Hoover', '1929-03-04', '1933-03-04', 3525.25), (32, 4, 'Franklin', 'Roosevelt', '1933-03-04', '1945-04-12', 35293734.35), (33, 4, 'Harry', 'Truman', '1945-04-12', '1953-01-20', 23579358.35), (34, 6, 'Dwight', 'Eisenhower', '1953-01-20', '1961-01-20', 25973535.35), (35, 4, 'John', 'Kennedy', '1961-01-20', '1963-11-22', 46081.24), (36, 4, 'Lyndon', 'Johnson', '1963-11-22', '1969-01-20', 2503759.35), (37, 6, 'Richard', 'Nixon', '1969-01-20', '1974-08-09', 3259744.53), (38, 6, 'Gerald', 'Ford', '1974-08-09', '1977-01-20', 643076.05), (39, 4, 'Jimmy', 'Carter', '1977-01-20', '1981-01-20', 1205735.25), (40, 6, 'Ronald', 'Reagan', '1981-01-20', '1989-01-20', 99867297.35), (41, 6, 'George H.', 'Bush', '1989-01-20', '1993-01-20', 92048204.24), (42, 4, 'Bill', 'Clinton', '1993-01-20', '2001-01-20', 12073975.24);
Note: We can insert this data manually into the database. Or this script can be executed automatically when installing the module. (See section Installation and upgrades)
Building the models
We have 2 types of data that is going to be used on this module President and Party For this we will create to Model classes in the modules/presidents/models folder
Party.php
<?php /** * The Party model * * @property int $id * @property string $name */ class GO_Presidents_Model_Party extends GO_Base_Db_ActiveRecord { /** * Returns a static model of itself * * @param String $className * @return GO_Presidents_Model_Party */ public static function model($className=__CLASS__) { return parent::model($className); } public function tableName() { return 'pm_parties'; } public function relations() { return array( 'presidents' => array('type' => self::HAS_MANY, 'model' => 'GO_Presidents_Model_President', 'field' => 'party_id', 'delete' => true) ); } }
Our model class will extend from GO_Base_Db_ActiveRecord. Also the name of the class is important and should be in the following format: GO_[modulename]_Model_[modelname] where model name should be the same as the filename.
This is all there is to create a model class. The properties of the model are already known since the database table is specified in the tableName() function
Also a relation is defined between the Party Model and the President model. Create the president model as follows:
President.php
<?php /** * The President model * * @property int $id * @property int $party_id * @property string $firstname * @property string $lastame * @property date $tookoffice * @property date $leftoffice * @property float $income */ class GO_Presidents_Model_President extends GO_Base_Db_ActiveRecord { /** * Returns a static model of itself * * @param String $className * @return GO_Presidents_Model_President */ public static function model($className=__CLASS__) { return parent::model($className); } protected function init() { $this->columns["income"]["gotype"]="number"; return parent::init(); } public function tableName(){ return 'pm_presidents'; } public function relations(){ return array( 'party' => array('type'=>self::BELONGS_TO, 'model'=>'GO_Presidents_Model_Party', 'field'=>'party_id'), ); } }
That's all there is to creating models for this example module
Creating the controllers
The controllers we stick into the 'modules/presidents/controller' folder
Create the following 2 files:
PartyController.php
<?php class GO_Presidents_Controller_Party extends GO_Base_Controller_AbstractModelController{ protected $model = 'GO_Presidents_Model_Party'; }
PresidentController.php
<?php class GO_Presidents_Controller_President extends GO_Base_Controller_AbstractModelController { protected $model = 'GO_Presidents_Model_President'; /** * Tell the controller to change some column values */ protected function formatColumns(GO_Base_Data_ColumnModel $columnModel) { $columnModel->formatColumn('party_id','$model->party->name'); $columnModel->formatColumn('income_val','$model->income'); return parent::formatColumns($columnModel); } /** * Display current value in combobox */ protected function remoteComboFields(){ return array('party_id'=>'$model->party->name'); } }
To the PartyController there is not much to understand. Extend from GO_Base_Controller_AbstractModelController Tell it what model class it should use and it knows how to Load, Save and Load JSON data for a grid.
In the President Controller 2 extra functions are added. formatColumns() Will make sure that in the datagrid won't show the id, but the name of the party that to president is member of.
remoteComboFields() This will make sure that the Combobox is filled with the right data when the President Dialog (Edit form) is shown.
Designing the views
Now for the fun part: The client-side of your module. We'll create the views that display on the screen. This is what our end-users will actually see:
We'll need 3 different view:
- a Datagrid for displaying all records
- a Form dialog for editing a record
- a Main panel where we will put the above.
We already created MainPanel.js in the modules/views/extjs3/ folder in the beginning of this guide. Lets modify that file with the following code:
MainPanel.js
// Creates namespaces to be used for scoping variables and classes so that they are not global. Ext.namespace('GO.presidents'); /* * This will add the module to the main tab-panel filled with all the modules */ GO.moduleManager.addModule( 'presidents', //Module alias GO.presidents.PresidentsGrid, //The main panel for this module { title : 'Presidents', //Module name in start-menu iconCls : 'go-module-icon-presidents' //The css class with icon for start-menu } );
This file will do no more then search for GO.presidents.PresidentsGrid to fill the modules MainPanel with.
For our GridPanel we are going to use a Group-Office class that extends Ext.grid.GridPanel.
GO.grid.GridPanel is an extension of the default Ext grid and implements some basic Group-Office functionality like deleting items.
We can now delete selected items in the grid with only the [delete] button.
Lets create this 'PresidentsGrid' in the modules/presidents/views/extjs3/ folder
PresidentsGrid.js
GO.presidents.PresidentsGrid = function(config){ if(!config) { config = {}; } //Construct the Gridpanel with the above configuration GO.presidents.PresidentsGrid.superclass.constructor.call(this, config); } //Extend the PresidentsGrid from GridPanel Ext.extend(GO.presidents.PresidentsGrid, GO.grid.GridPanel,{ });
This is the code that is needed for a data-grid but we haven't configured anything yet so it won't do anything for until we do.
For this grid we are going to configure the following properties:
- Some default properties like how to layout and we'll give it a title
- Set the Edit dialog class we will use to edit a record.
- A data-store from which the grid needs to pull its data
- A column model, what column we want to display and how.
Just before the 'Construct the Gridpanel with the above configuration' comment add the following code to configure what's listed above:
Some default values for the grid each commented with its use:
config.title = GO.presidents.lang.presidents; //Title of this panel config.layout='fit'; //How to lay out the panel config.autoScroll=true; config.split=true; config.autoLoadStore=true; config.sm=new Ext.grid.RowSelectionModel(); config.loadMask=true; //Mask the grid when it is loading data
Setting the editDialogClass this is rather important. We will create the PresidentDialog later on. Let's first finish configuring this grid.
//This dialog will be opened when double clicking a row or calling showEditDialog() config.editDialogClass = GO.presidents.PresidentDialog;
Setting the column model of the data-gird. What column do you want to display and what should be left out? For all the properties a ColumnModel can have please go to Ext.grid.ColumnModel This is just the basic usage.
//Configuring the column model config.cm = new Ext.grid.ColumnModel({ defaults:{ sortable: true }, columns : [{ header: '#', dataIndex: 'id', hidden:true, sortable: false },{ header: GO.presidents.lang.firstname, dataIndex: 'firstname' },{ header: GO.presidents.lang.lastname, dataIndex: 'lastname' },{ header: GO.presidents.lang.party_id, dataIndex: 'party_id' },{ header: GO.presidents.lang.tookoffice, dataIndex: 'tookoffice' },{ header: GO.presidents.lang.leftoffice, dataIndex: 'leftoffice' },{ header: GO.presidents.lang.income, dataIndex: 'income', align: "right" }] });
As you can see in the above code we created a ColumnModel object and added it to the config For the header value of the columns that will display in our view
Defining a data-store for the grid:
//Defining the data store for the grid config.store = new GO.data.JsonStore({ url: GO.url('presidents/president/store'), fields: ['id','firstname','lastname','party_id','tookoffice','leftoffice','income'], remoteSort: true });
Note: The url: GO.url('presidents/president/store') refers to the methode 'actionStore()' in the President Controller wich it inherits from the AbstractController class. The url will return a JSON string once called.
After setting the data-store we'll add the add the GridView component to the panel:
//Adding the gridview to the grid panel config.view=new Ext.grid.GridView({ emptyText: GO.lang.strNoItemse });
In this example we only set the emptyText value this is the text that gets displayed when no records are found by the data store. We are not using a string here but we'll reference to a global language string provided by Group-Office.
For fore info on the Grid view see the Ext.grid.GridView docs
Our data-grid is now ready for usage. Let's not forget to add our new PresidentsGrid to scripts.txt or else it wont be loaded by the system and or MainPanel.js cant find the Grid
scripts.txt:
modules/presidents/views/Extjs3/PresidentsGrid.js modules/presidents/views/Extjs3/MainPanel.js
Note: As the PresidentGrid class is used in MainPanel.js the PresidentsGrid.js file needs the be loaded first.
The data-grid should now work and display the presidents of the United States. Note that because of the 'formatColumns()' method we made in the PresidentController not the ID of a party will be displayed in the grid but the name of the party.
Creating a Dialog
Now we are going to create a Dialog which will make editing presidents very easy.
Create a file modules/president/module/PresidentDialog.js
PresidentDialog.js:
GO.presidents.PresidentDialog = Ext.extend(GO.dialog.TabbedFormDialog , { initComponent : function(){ Ext.apply(this, { titleField:'lastname', goDialogId:'president', title:GO.presidents.lang.president, width: 300, height: 280, formControllerUrl: 'presidents/president' }); GO.presidents.PresidentDialog.superclass.initComponent.call(this); } });
This will create an empty dialog that will open when we double click a row in the grid. But it doesn't work yet. This is because we haven't loaded the PresidentDialog.js file. Lets do this by changing your 'scripts.txt' file again.
scripts.txt:
modules/presidents/views/Extjs3/PresidentsGrid.js modules/presidents/views/Extjs3/PresidentDialog.js modules/presidents/views/Extjs3/MainPanel.js
Adding some language strings
In the PresidentDialog.js file you can see that the title property of the dialog is set to GO.presidents.lang.president This language string doesn't exists yet. So let's add some translatable strings that we can use in our PresidentDialog.
Open up modules/language/en.php and write the following:
en.php:
$l = array( 'name' => ' Presidents', //this name will appear in the Start menu -> modules section 'description' => 'A simple test module', //this description will appear in the Start menu -> modules section 'presidents' => 'Presidents', 'president' => 'President', 'party_id' => 'Party', 'tookoffice' => 'Entering Office', 'leftoffice' => 'Leaving Office', 'income' => 'Income', 'firstname' => 'First name', 'lastname' => 'Last name', );
GO.presidents.lang.president in the title of our PresidentDialog will now display: "President" The other language strings you see will be used for the form that will be created inside the dialog.
Creating a form inside the dialog
In the PresidentDialog.js file we have created a PresidentDialog extended from TabbedFormDialog and overwritten the function initComponents and set some properties there.
We are going to overwrite one more method in the PresidentDialog. Add a comma (,) at the end of the initComponent function and add the following code afterwards:
PresidentDialog.js:
buildForm : function () { this.propertiesPanel = new Ext.Panel({ title:GO.lang.strProperties, cls:'go-form-panel', layout:'form', items:[{ xtype: 'textfield', name: 'firstname', width:300, anchor: '100%', maxLength: 100, allowBlank:false, fieldLabel: GO.presidents.lang.firstname }, { xtype: 'textfield', name: 'lastname', fieldLabel: GO.presidents.lang.lastname, anchor: '100%' }, { xtype:'combo', fieldLabel: GO.presidents.lang.party_id, hiddenName:'party_id', anchor:'100%', emptyText:GO.lang.strPleaseSelect, store: new GO.data.JsonStore({ url: GO.url('presidents/party/store'), baseParams: { permissionLevel:GO.permissionLevels.write }, fields: ['id', 'name'] }), valueField:'id', displayField:'name', triggerAction: 'all', editable: true, forceSelection: true, allowBlank: false }, { xtype: 'datefield', name: 'tookoffice', fieldLabel: GO.presidents.lang.tookoffice, allowBlank: false, anchor: '100%' }, { xtype: 'datefield', name: 'leftoffice', fieldLabel: GO.presidents.lang.leftoffice, allowBlank: false, anchor: '100%' }, { xtype: 'numberfield', name: 'income', value: GO.util.numberFormat(0), fieldLabel: GO.presidents.lang.income, allowBlank: false, anchor: '100%' }] }); this.addPanel(this.propertiesPanel); }
This will add a panel to our PresidentDialog and add 6 form-fields inside. The combo/dropdown field is a bit tricky.
It's needs to know where to pull the data that it is going to list from. for this we set the store property of the field and fill it with a JsonStore object. This object needs to know a controller action that returns JSON data. We'll refer to the 'store' action in the PartyController we've created earlier. Also we specify the fields the be used for the key and value of the dropdown menu. In this case 'id' and 'name'.
When double clicking a row the President dialog will now show a form with the above defined fields.
In the next section we are going to add a toolbar which will allow us to create new presidents.
Adding a Toolbar
We are now going to add a toolbar in the top with links to create or delete a president.
All of the following code has to be added within the PresidentsGrid configuration.
PresidentsGrid.js:
//Setup a toolbar for the grid panel config.tbar = new Ext.Toolbar({ cls:'go-head-tb', items: [ { iconCls: 'btn-add', text: GO.lang['cmdAdd'], cls: 'x-btn-text-icon', handler: function(){ this.showEditDialog(); }, scope: this }, { iconCls: 'btn-delete', text: GO.lang['cmdDelete'], cls: 'x-btn-text-icon', handler: function(){ this.deleteSelected(); }, scope: this }, GO.lang.strSearch + ':', new GO.form.SearchField({ store: config.store, width:320 }) ] });
Setting a Ext.Toolbar object to the tbar property of the GridPanel will render a toolbar on top of the grid. In this example we'll add 4 items to the toolbar:
- An add button that will call the PresidentDialog but this time it's empty and can be used to add a new record
- A delete button that will delete the selected row.
- A string
- A search Field.
For more information about the see the Ext.Toolbar docs
Adding a PagingToolbar
This one is very easy to do because we used a GO.grid.GridPanel instead of the Ext one.
We only need to add 1 line to our PresidentsGrid configuration and our PHP takes care of the rest.
PresidentsGrid.js:
/* config.paging is an added functionality by Group-Office * which makes it very easy to add a PagingToolbar. */ config.paging=true,
Adding some style
Before we start styling we need to create a few more folders.
Create Directories:
modules/presidents/themes/ modules/presidents/themes/Default/
Now we create the file modules/presidents/themes/Default/style.css
Most modules will want to have it's own module-icon, so we add this default rule for later use.
style.css:
/* This class is used to show the icon in the main tabpanel of GO */ .go-module-icon-presidents { }
In our Grid we can't change the ID's of presidents. To make it apparent that this field is treated differently we will add a renderer that sets the cell CSS style so it will use a different background.
To do this we need to change the corresponding column config object in our PresidentsGrid ColumnModel.
PresidentsGrid.js:
{ header: '#', readOnly: true, dataIndex: 'id', renderer: function(value, cell){ cell.css = "readonlycell"; return value; }, width: 50 }
In our style.css we add a .readonlycell rule
style.css:
.readonlycell { background-color:#CCCCCC !important; }
To make it more clear which president had the better income we will change the color of the income according to its value. We need to change the renderer function of the Income column.
PresidentsGrid.js:
{ header: "Income", dataIndex: 'income', width: 120, renderer: function(value, metaData, record){ if(record.data.income_val > 1000000){ metaData.attr = 'style="color:#336600;"'; } else if (record.data.income_val > 100000){ metaData.attr = 'style="color:#FF9900;"'; } else { metaData.attr = 'style="color:#CC0000;"'; } return "$ "+value; }, align: "right" }
This concludes this tutorial.
We now have a fully functional module which lets us add, update and delete presidents.
Putting custom fields in module items
This will only work with the professional version and you must have the Custom fields module installed. To create custom fields for a model, we must create a custom field module. We'll place it in the following directory: modules/presidents/customfields/model/
President.php
class GO_Presidents_Customfields_Model_President extends GO_Customfields_Model_AbstractCustomFieldsRecord{ /** * Returns a static model of itself * * @param String $className * @return GO_Presidents_Model_CustomFieldsRecord */ public static function model($className=__CLASS__) { return parent::model($className); } public function extendsModel(){ return "GO_Presidents_Model_President"; } }
Our President model still needs to know what the classname of the CustomerFields model is. For this we'll add the Following method the our President Model:
President.php
public function customfieldsModel(){ return "GO_Presidents_Customfields_Model_President"; }
Also the customer fields we specify need to be saved into the database For this we'll create a table named 'cf_pm_presidents':
CREATE TABLE IF NOT EXISTS `cf_pm_presidents` ( `model_id` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`model_id`) ); INSERT INTO `cf_pm_presidents` (`model_id`) VALUES (1);
Note: The table name needs to be the same as the table name where the records are saved but prefixed with ' cf_'
This is all there is to add custom field functionality. After this we still need to change our views to display the input fields in our edit dialog. Add the following line just before the initComponent function
PresidentDialog.php
customFieldType : "GO_Presidents_Model_President",
This should make the customfields work all we have to do is create some fields. Install the Custom fields module if you haven't done this already. This module is only available in the pro version of Group-Office.
When it's installed it will show up in the Admin menu of the start-menu. Start it and in the left sidebar you will see the the President Model is there. Click it and add a Category. Name it: 'Appearance'.
We'll add the following field to this category: Name: Hair color, Type: Text Name: Birthday, Type: Date Name: Glasses, Type: Checkbox
To check if the fields will work. We first need to reload Group Office. Refresh the page, go to the Presidents module and edit a record. See the extra tab Appearance in to PresidentDialog?
It's also possible to display customfields in the data-grid if you like. The reason that the fields aren't displaying in the grid yet is because the DataStore of the grid doesn't know what the model type is. It only has attributes.
Lets change to config.store attribute of the configuration of our PresidentGrid PresidentGrid.js:
//Defining the data store for the grid config.store = new GO.data.JsonStore({ url: GO.url('presidents/president/store'), fields: ['id','firstname','lastname','party_id','tookoffice','leftoffice','income', 'income_val'], remoteSort: true, model: 'GO_Presidents_Model_President' });
All your defines customfields will be visible into the data-grid. Note: The following fieldtypes will never be rendered in the grid: 'textarea','html', 'heading', 'infotext'
Making your items linkable
In Group-Office we can link one item to another. If your newly create module has an item that should be linkable we add the following overwrite the hasLinks() method in our model
We are going to make the presidents linkable, for this we add the following methods to our President Model:
President.php
public function hasLinks() { return true; } public function getFullname() { return $this->firstname . " " . $this->lastname; } protected function getCacheAttributes() { return array("name"=>$this->fullname, "description"=>$this->party->name); }
Next our database needs a table where to links for the presidents will be saved. We execute the following query on our database:
SQL
CREATE TABLE IF NOT EXISTS `go_links_pm_presidents` ( `id` int(11) NOT NULL, `folder_id` int(11) NOT NULL, `model_id` int(11) NOT NULL, `model_type_id` int(11) NOT NULL, `description` varchar(100) DEFAULT NULL, `ctime` int(11) NOT NULL, KEY `link_id` (`model_id`,`model_type_id`), KEY `id` (`id`,`folder_id`), KEY `ctime` (`ctime`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Note: the table name is the same as where the president records are stored but prefixed with 'go_links_'
In the PresidentDialog we will add a field to add links to the record after the income numberfield we'll add a new Go.form.SelectLink componentent. To code will look like this:
PresidentDialog.js
{ xtype: 'numberfield', name: 'income', value: GO.util.numberFormat(0), fieldLabel: GO.presidents.lang.income, allowBlank: false, anchor: '100%' }, new GO.form.SelectLink({ anchor:'100%' })
This is all what's needed for adding other linkable Group-Office record to the president record. But where are they? Let's make a panel to display the links. We'll place this on the right side of the grid.
The MainPanel will be seperated into 2 divided panels. Our PresidentGrid in the middle and the (still to build) PresidentPanel on the right. Let's build the PresidentPanel
PresidentPanel.js
GO.presidents.PresidentPanel = Ext.extend(GO.DisplayPanel,{ model_name : "GO_Presidents_Model_President", noFileBrowser : true, stateId : 'pm-president-panel', editHandler : function(){ if(!GO.presidents.presidentDialog) GO.presidents.presidentDialog = new GO.presidents.PresidentDialog(); GO.presidents.presidentDialog.show(this.link_id); }, initComponent : function(){ this.loadUrl=GO.url('presidents/president/display'); this.template = '<table class="display-panel" cellpadding="0" cellspacing="0" border="0">'+ '<tr>'+ '<td colspan="2" class="display-panel-heading">{firstname} {lastname}</td>'+ '</tr>'+ '<tr>'+ '<td>ID:</td>'+ '<td>{id}</td>'+ '</tr>'+ '<tr>'+ '<td>'+GO.presidents.lang.party_id+':</td>'+ '<td>{partyName}</td>'+ '</tr>'+ '<tr>'+ '<td>'+GO.presidents.lang.tookoffice+':</td>'+ '<td>{tookoffice}</td>'+ '</tr>'+ '<tr>'+ '<td>'+GO.presidents.lang.leftoffice+':</td>'+ '<td>{leftoffice}</td>'+ '</tr>'+ '<tr>'+ '<td>'+GO.presidents.lang.income+':</td>'+ '<td>{income}</td>'+ '</tr>'+ '</table>'; if(GO.customfields) { this.template +=GO.customfields.displayPanelTemplate; } if(GO.tasks) this.template +=GO.tasks.TaskTemplate; if(GO.calendar) this.template += GO.calendar.EventTemplate; if(GO.workflow) this.template +=GO.workflow.WorkflowTemplate; this.template += GO.linksTemplate; Ext.apply(this.templateConfig, GO.linksTemplateConfig); if(GO.comments) { this.template += GO.comments.displayPanelTemplate; } GO.presidents.PresidentPanel.superclass.initComponent.call(this); } });
After creating this Panel we still have to load it when our module loads. Add the following line on top of the scripts.txt file in your view folder.
scripts.txt
modules/presidents/views/Extjs3/PresidentPanel.js
Lets make a split panel. where the center is our grid listing presidents. en the right panel will display the presidents details and links. Change the code in MainPanel.js to the following:
MainPanel.js
// Creates namespaces to be used for scoping variables and classes so that they are not global. Ext.namespace('GO.presidents'); GO.presidents.MainPanel = function(config){ if(!config) { config = {}; } this.centerPanel = new GO.presidents.PresidentsGrid({ region:'center', id:'pm-center-panel', border:true }); this.centerPanel.on("delayedrowselect",function(grid, rowIndex, r){ this.eastPanel.load(r.data.id); }, this); this.centerPanel.store.on('load', function(){ this.eastPanel.reset(); }, this); this.eastPanel = new GO.presidents.PresidentPanel({ region:'east', id:'pm-east-panel', width:440, border:true }); //Setup a toolbar for the grid panel config.tbar = new Ext.Toolbar({ cls:'go-head-tb', items: [ { iconCls: 'btn-add', text: GO.lang['cmdAdd'], cls: 'x-btn-text-icon', handler: function(){ this.centerPanel.showEditDialog(); }, scope: this }, { iconCls: 'btn-delete', text: GO.lang['cmdDelete'], cls: 'x-btn-text-icon', handler: function(){ this.centerPanel.deleteSelected(); }, scope: this } ] }); config.items=[ this.centerPanel, this.eastPanel ]; config.layout='border'; GO.presidents.MainPanel.superclass.constructor.call(this, config); }; Ext.extend(GO.presidents.MainPanel, Ext.Panel, {}); /* * This will add the module to the main tabpanel filled with all the modules */ GO.moduleManager.addModule( 'presidents', //Module alias GO.presidents.MainPanel, //The main panel for this module { title : 'Presidents', //Module name in startmenu iconCls : 'go-module-icon-presidents' //The css class with icon for startmenu } ); /* * Add linkHandeler for linking models from this module to other GroupOffice models */ GO.linkHandlers["GO_Presidents_Model_President"]=function(id){ if(!GO.presidents.linkWindow){ var presidentPanel = new GO.presidents.PresidentPanel(); GO.presidents.linkWindow= new GO.LinkViewWindow({ title: GO.presidents.lang.president, items: presidentPanel, presidentPanel: presidentPanel, closeAction:"hide" }); } GO.presidents.linkWindow.presidentPanel.load(id); GO.presidents.linkWindow.show(); return GO.presidents.linkWindow; }
As you can see in the New MainPanel.js view this panel has a 'config.tbar' defines with the 2 'add' and 'delete' buttons. Note that the reference to the showEditDialog() method has changed to 'this.centerPanel' instead of 'this' because the centerPanel now represents the PresidentsGrid.
So this buttons can be removed from the toolbar in the PresidentGrid.js
So change to config.tbar option in the PresidentGrid.js to the following.
PresidentGrid.js:
config.tbar = new Ext.Toolbar({ items: [ GO.lang.strSearch + ':', new GO.form.SearchField({ store: config.store, width:320 }) ] })
This way only the searchfield will remain in the toolbar of the PresidentsGrid. The Add and Delete button are already placed on the MainPanel's toolbar
Extending the defaults
Do you see the Add and Edit button in top of your panel being grayed out. The main reason for this it because the edit panel has build in functionality the check if you have permission the read or write a record. this way you can set this permission per record.
For our presidents example it's not very logic to have these permission per record. We didn't specify this in the database and are not going to use it in this example. for this reason. we need to overwrite the data that we get from the server.
Lets have o look at what's returned:
{"data": {"id":"40", "party_id":"6", "firstname":"Ronald", "lastname":"Reagan", "tookoffice":"20-01-1981", "leftoffice":"20-01-1989", "income":"99,867,297.35", "model":"GO_Presidents_Model_President", "permission_level":4, "write_permission":true, "customfields":[{ "id":"1", "name":"Appearance", "fields":[{ "name":"Glasses", "value":"No" }] }], "links":[], "events":[], "tasks":[], "files":[], "comments":[] }, "success":true}
All display view in Group-Office are ExtJS components or Group-Office components that are extended from ExtJS components. All of these components are filled with JSON data. This data is loaded Asynchrone with Javascript. Let look at what data will be loaded
Let's modify the JSON data that is returned. Add the following function to the PresidentController
PresidentController.php:
protected function afterDisplay(&$response, &$model, &$params) { $response['data']['partyName'] = $model->party->name; return parent::beforeDisplay($response, $model, $params); }
With this the JSON data that is returned in actionDisplay() will be overwritten. a field partyName will be added to our returned JSON data. this will be used to display the party name in the PresidentPanel
Installation and upgrades
Files in the modules/presidents/install folder are important for installing, uninstalling and upgrading modules. install.sql and uninstall.sql are automatically applied to the database when installing or uninstalling. updates.php is used for upgrading.
It looks like this for example:
<?php $updates["201108131011"][]="ALTER TABLE `ab_companies` CHANGE `id` `id` INT( 11 ) NOT NULL AUTO_INCREMENT"; $updates["201108131011"][]="ALTER TABLE `ab_contacts` CHANGE `id` `id` INT( 11 ) NOT NULL AUTO_INCREMENT"; $updates["201108131011"][]="ALTER TABLE `ab_contacts` ADD `go_user_id` INT NOT NULL , ADD INDEX ( `go_user_id` )"; $updates["201204270920"][]="script:1_some_script.php";
The timestamp is in this format:
"YYYYMMDDHHMM"
The timestamp is important because all updates are executed chronologically. Because modules can depend on each others updates.
You can also use scripts. It's good practice to avoid using update scripts as much as possible. When scripts use functions of the framework and they change later they might break. The entry "script:1_some_script.php" will include modules/[moduleid]/install/updatescripts/1_some_script.php.
Note: When putting update queries in the updates.php file, don't forget to update install.sql too.