فهرست منبع

Customizable tables implementation...

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@2127 a333f486-631f-4898-b8df-5754b55c2be0
dflaven 13 سال پیش
والد
کامیت
469b22dd7e

+ 47 - 338
application/cmdbabstract.class.inc.php

@@ -39,6 +39,7 @@ require_once(APPROOT.'/application/ui.linkswidget.class.inc.php');
 require_once(APPROOT.'/application/ui.passwordwidget.class.inc.php');
 require_once(APPROOT.'/application/ui.extkeywidget.class.inc.php');
 require_once(APPROOT.'/application/ui.htmleditorwidget.class.inc.php');
+require_once(APPROOT.'/application/datatable.class.inc.php');
 
 /**
  * All objects to be displayed in the application (either as a list or as details)
@@ -244,7 +245,8 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
 			// Display mode
 			if (!$oAttDef->IsLinkset()) continue; // Process only linkset attributes...
 			
-			$oSet = new DBObjectSet($this->Get($sAttCode)->GetFilter());
+			// $oSet = new DBObjectSet($this->Get($sAttCode)->GetFilter()); // Why do something so useless ?
+			$oSet = $this->Get($sAttCode);
 			$iCount = $oSet->Count();
 			$sCount = '';
 			if ($iCount != 0)
@@ -679,7 +681,6 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
 		$sHtml = '';
 		$oAppContext = new ApplicationContext();
 		$sClassName = $oSet->GetFilter()->GetClass();
-		$aAttribs = array();
 		$sZListName = isset($aExtraParams['zlist']) ? ($aExtraParams['zlist']) : 'list';
 		if ($sZListName !== false)
 		{
@@ -702,9 +703,6 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
 			}
 		}
 
-		// Load only the requested columns
-		$sClassAlias = $oSet->GetFilter()->GetClassAlias();
-		$oSet->OptimizeColumnLoad(array($sClassAlias => $aList));
 
 		if (!empty($sLinkageAttribute))
 		{
@@ -740,270 +738,45 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
 			// Then display all the attributes linked to the other end of the relationship
 			$aList = $aDisplayList;
 		}
+		
+		$sSelectMode = 'none';
 		if ($bSelectMode)
 		{
-			if (!$bSingleSelectMode)
-			{
-				$aAttribs['form::select'] = array('label' => "<input type=\"checkbox\" onClick=\"CheckAll('.selectList{$iListId}:not(:disabled)', this.checked);\" class=\"checkAll\"></input>", 'description' => Dict::S('UI:SelectAllToggle+'));
-			}
-			else
-			{
-				$aAttribs['form::select'] = array('label' => "", 'description' => '');
-			}
-		}
-		if ($bViewLink)
-		{
-			$aAttribs['key'] = array('label' => MetaModel::GetName($sClassName), 'description' => '');
-		}
-		foreach($aList as $sAttCode)
-		{
-			$oAttDef = MetaModel::GetAttributeDef($sClassName, $sAttCode);
-			$aAttribs[$sAttCode] = array('label' => MetaModel::GetLabel($sClassName, $sAttCode), 'description' => $oAttDef->GetOrderByHint());
+			$sSelectMode = $bSingleSelectMode ? 'single' : 'multiple';
 		}
-		$aValues = array();
+		
+		$sClassAlias = $oSet->GetClassAlias();
 		$bDisplayLimit = isset($aExtraParams['display_limit']) ? $aExtraParams['display_limit'] : true;
-		$iMaxObjects = -1;
-		if ($bDisplayLimit && ($oSet->Count() > MetaModel::GetConfig()->GetMaxDisplayLimit()))
-		{
-			$iMaxObjects = MetaModel::GetConfig()->GetMinDisplayLimit();
-			$oSet->SetLimit($iMaxObjects);
-		}
-		$oSet->Seek(0);
-		while (($oObj = $oSet->Fetch()) && ($iMaxObjects != 0))
-		{
-			$aRow = array();
-			$sHilightClass = $oObj->GetHilightClass();
-			if ($sHilightClass != '')
-			{
-				$aRow['@class'] = $sHilightClass;	
-			}
-			if ($bViewLink)
-			{
-				$aRow['key'] = $oObj->GetHyperLink();
-			}
-			if ($bSelectMode)
-			{
-				if (array_key_exists('selection_enabled', $aExtraParams) && isset($aExtraParams['selection_enabled'][$oObj->GetKey()]))
-				{
-					$sDisabled = ($aExtraParams['selection_enabled'][$oObj->GetKey()]) ? '' : ' disabled="disabled"';
-				}
-				else
-				{
-					$sDisabled = '';
-				}
-				if ($bSingleSelectMode)
-				{
-					$aRow['form::select'] = "<input type=\"radio\" $sDisabled class=\"selectList{$iListId}\" name=\"selectObject\" value=\"".$oObj->GetKey()."\"></input>";
-				}
-				else
-				{
-				$aRow['form::select'] = "<input type=\"checkBox\" $sDisabled class=\"selectList{$iListId}\" name=\"selectObject[]\" value=\"".$oObj->GetKey()."\"></input>";
-				}
-			}
-			foreach($aList as $sAttCode)
-			{
-				$aRow[$sAttCode] = $oObj->GetAsHTML($sAttCode);
-			}
-			$aValues[] = $aRow;
-			$iMaxObjects--;
-		}
-		$sHtml .= '<table class="listContainer">';
-		$sColspan = '';
-
-		$sFilter = $oSet->GetFilter()->serialize();
-		$iMinDisplayLimit = MetaModel::GetConfig()->GetMinDisplayLimit();
-		$sCollapsedLabel = Dict::Format('UI:TruncatedResults', $iMinDisplayLimit, $oSet->Count());
-		$sLinkLabel = Dict::S('UI:DisplayAll');
-		foreach($oSet->GetFilter()->GetInternalParams() as $sName => $sValue)
-		{
-			$aExtraParams['query_params'][$sName] = $sValue;
-		}
-
-		if ($bDisplayMenu)
-		{
-			$oMenuBlock = new MenuBlock($oSet->GetFilter());
-			$sColspan = 'colspan="2"';
-			$aMenuExtraParams = $aExtraParams;
-			if (!empty($sLinkageAttribute))
-			{
-				//$aMenuExtraParams['linkage'] = $sLinkageAttribute;
-				$aMenuExtraParams = $aExtraParams;
-			}
-			$sHtml .= $oMenuBlock->GetRenderContent($oPage, $aMenuExtraParams, $iListId);
-			$sHtml .= '</td></tr>';
-		}
-		$sHtml .= "<tr><td $sColspan>";
-		$sHtml .= $oPage->GetTable($aAttribs, $aValues);
-		$sHtml .= '</td></tr>';
-		$sHtml .= '</table>';
-		$iCount = $oSet->Count();
-		if ($bSelectMode)
-		{
-			$sHeader = Dict::Format('UI:Pagination:HeaderSelection', '<span id="total">'.$iCount.'</span>', '<span class="selectedCount">0</span>');
-		}
-		else
-		{
-			$sHeader = Dict::Format('UI:Pagination:HeaderNoSelection', '<span id="total">'.$iCount.'</span>');
-		}
-
-		// All lists are now paginated in order to benefit from the SQL sort order
-
-		if (!$bDisplayLimit)
-		{
-			$sPagerStyle = 'style="display:none"'; // no limit: display the full table, so hide the "pager" UI
-			$iPageSize = -1; // Display all
-		}
-		else
-		{
-			$sPagerStyle = '';
-			$iPageSize = MetaModel::GetConfig()->GetMinDisplayLimit();
-		}
-		$iDefaultPageSize =  MetaModel::GetConfig()->GetMinDisplayLimit();
-		$sCombo = '<select class="pagesize">';
-		for($iPage = 1; $iPage < 5; $iPage++)
-		{
-			$sSelected = ($iPage == $iPageSize) ? 'selected="selected"' : '';
-			$iNbItems = $iPage * $iDefaultPageSize;
-			$sCombo .= "<option  $sSelected value=\"$iNbItems\">$iNbItems</option>";
-		}
-		$sSelected = (-1 == $iPageSize) ? 'selected="selected"' : '';
-		$sCombo .= "<option  $sSelected value=\"-1\">".Dict::S('UI:Pagination:All')."</option>";
-		$sCombo .= '</select>';
-		$sPages = Dict::S('UI:Pagination:PagesLabel');
-		$sPageSizeCombo = Dict::Format('UI:Pagination:PageSize', $sCombo);
 		
-		$iNbPages = ($iPageSize == -1) ? 1 : ceil($iCount / $iPageSize);
-		$aPagesToDisplay = array();
-		for($idx = 0; $idx <= min(4, $iNbPages-1); $idx++)
-		{
-			if ($idx == 0)
-			{
-				$aPagesToDisplay[$idx] = '<span page="0" class="curr_page">1</span>';
-			}
-			else
-			{
-				$aPagesToDisplay[$idx] = "<span id=\"gotopage_$idx\" class=\"gotopage\" page=\"$idx\">".(1+$idx)."</span>";
-			}
-		}
-		$iLastPageIdx = $iNbPages - 1;
-		if (!isset($aPagesToDisplay[$iLastPageIdx]))
-		{
-			unset($aPagesToDisplay[$idx - 1]); // remove the last page added to make room for the very last page
-			$aPagesToDisplay[$iLastPageIdx] = "<span id=\"gotopage_$iLastPageIdx\" class=\"gotopage\" page=\"$iLastPageIdx\">... $iNbPages</span>";
-		}
-		$sPagesLinks = implode('', $aPagesToDisplay);
-		$sPagesList = '['.implode(',', array_keys($aPagesToDisplay)).']';
-
-		$sSelectionMode = ($iNbPages == 1) ? '' : 'positive';
-		$sHtml =
-<<<EOF
-<div id="pager{$iListId}" class="pager" $sPagerStyle>
-		<p>$sHeader</p>
-		<p><table class="pagination"><tr><td>$sPages</td><td><img src="../images/first.png" class="first"/></td>
-		<td><img src="../images/prev.png" class="prev"/></td>
-		<td><span id="index">$sPagesLinks</span></td>
-		<td><img src="../images/next.png" class="next"/></td>
-		<td><img src="../images/last.png" class="last"/></td>
-		<td>$sPageSizeCombo</td>
-		<td><span id="loading">&nbsp;</span></td>
-		</tr>
-		</table>
+		// Load only the requested columns
+		$oSet->OptimizeColumnLoad(array($sClassAlias => $aList));
 		
-		<input type="hidden" name="selectionMode" value="$sSelectionMode"></input>
-</div>
-EOF
-.$sHtml;
-		$aArgs = $oSet->GetArgs();
-		$sExtraParams = addslashes(str_replace('"', "'", json_encode(array_merge($aExtraParams, $aArgs)))); // JSON encode, change the style of the quotes and escape them
-		$sSelectMode = '';
-		$sHeaders = '';
-		if ($bSelectMode)
-		{
-			$sSelectMode = $bSingleSelectMode ? 'single' : 'multiple';
-			$sHeaders = 'headers: { 0: {sorter: false}},';
-		}
-		$sDisplayKey = ($bViewLink) ? 'true' : 'false';
-		// Protect against duplicate elements in the Zlist
-		$aUniqueOrderedList = array();
-		foreach($aList as $sAttCode)
-		{
-			$aUniqueOrderedList[$sAttCode] = true;
-		}
-		$aUniqueOrderedList = array_keys($aUniqueOrderedList);
-		$sDisplayList = json_encode($aUniqueOrderedList);
-		$sCssCount = isset($aExtraParams['cssCount']) ? ", cssCount: '{$aExtraParams['cssCount']}'" : '';
-		$oSet->ApplyParameters();
-		// Display the actual sort order of the table
-		$aRealSortOrder = $oSet->GetRealSortOrder();
-		$aDefaultSort = array();
-		$iColOffset = 0;
-		if ($bSelectMode)
-		{
-			$iColOffset += 1;
-		}
-		if ($bViewLink)
-		{
-			$iColOffset += 1;
-		}
-		foreach($aRealSortOrder as $sColCode => $bAscending)
-		{
-			$iPos = array_search($sColCode, $aUniqueOrderedList);
-			if ($iPos !== false)
-			{
-				$aDefaultSort[] = "[".($iColOffset+$iPos).",".($bAscending ? '0' : '1')."]";
-			}
-			else if($sColCode == 'friendlyname' && $bViewLink)
-			{
-				$aDefaultSort[] = "[".($iColOffset-1).",".($bAscending ? '0' : '1')."]";
-			}
-		}
-		$sSortList = '';
-		if (count($aDefaultSort) > 0)
-		{
-			$sSortList = ', sortList: ['.implode(',', $aDefaultSort).']';
-		}
-		$sOQL = addslashes($oSet->GetFilter()->serialize());
-		$oPage->add_ready_script(
-<<<EOF
-var oTable = $('#{$iListId} table.listResults');
-oTable.tablesorter( { $sHeaders widgets: ['myZebra', 'truncatedList'] $sSortList} ).tablesorterPager({container: $('#pager{$iListId}'), totalRows:$iCount, size: $iPageSize, filter: '$sOQL', extra_params: '$sExtraParams', select_mode: '$sSelectMode', displayKey: $sDisplayKey, displayList: $sDisplayList $sCssCount});
-EOF
-		);
+		$sTableId = isset($aExtraParams['table_id']) ? $aExtraParams['table_id'] : null;
+		$aClassAliases = array( $sClassAlias => $sClassName);
+		$oDataTable = new DataTable($iListId, $oSet, $aClassAliases, $sTableId);
+		$oSettings = DataTableSettings::GetDataModelSettings($aClassAliases, $bViewLink, array($sClassAlias => $aList));
 		
-		if ($iNbPages == 1)
+		if ($bDisplayLimit)
 		{
-			if (isset($aExtraParams['cssCount']))
-			{
-				$sCssCount = $aExtraParams['cssCount'];
-				if ($bSingleSelectMode)
-				{
-					$sSelectSelector = ":radio[name^=selectObj]";
-				}
-				else
-				{
-					$sSelectSelector = ":checkbox[name^=selectObj]";
-				}
-				$oPage->add_ready_script(
-<<<EOF
-	$('#{$iListId} table.listResults $sSelectSelector').change(function() {
-		var c = $('{$sCssCount}');							
-		var v = $('#{$iListId} table.listResults $sSelectSelector:checked').length;
-		c.val(v);
-		$('#{$iListId} .selectedCount').text(v);
-		c.trigger('change');	
-	});
-EOF
-				);
-			}
+			$iDefaultPageSize = MetaModel::GetConfig()->GetMinDisplayLimit(); //TODO use user's prefs instead if any
+			$oSettings->iDefaultPageSize = $iDefaultPageSize;
 		}
-
-		return $sHtml;
+		
+		$oSettings->aSortOrder = MetaModel::GetOrderByDefault($sClassName);
+		
+		return $oDataTable->Display($oPage, $oSettings, $bDisplayMenu, $sSelectMode, $bViewLink, $aExtraParams);
 	}
 	
 	public static function GetDisplayExtendedSet(WebPage $oPage, CMDBObjectSet $oSet, $aExtraParams = array())
 	{
-		static $iListId = 0;
-		$iListId++;
+		if (empty($aExtraParams['currentId']))
+		{
+			$iListId = $oPage->GetUniqueId(); // Works only if not in an Ajax page !!
+		}
+		else
+		{
+			$iListId = $aExtraParams['currentId'];
+		}
 		$aList = array();
 		
 		// Initialize and check the parameters
@@ -1046,7 +819,7 @@ EOF
 			}
 		}
 		$aAttribs = array();
-		foreach($aAuthorizedClasses as $sAlias => $sClassName) // TO DO: check if the user has enough rights to view the classes of the list...
+		foreach($aAuthorizedClasses as $sAlias => $sClassName)
 		{
 			if (array_key_exists($sAlias, $aExtraFields))
 			{
@@ -1073,14 +846,8 @@ EOF
 					unset($aList[$sAlias][$index]);
 				}
 			}
-			if ($bViewLink)
-			{
-				$aAttribs['key_'.$sAlias] = array('label' => MetaModel::GetName($sClassName), 'description' => '');
-			}
-			foreach($aList[$sAlias] as $sAttCode)
-			{
-				$aAttribs[$sAttCode.'_'.$sAlias] = array('label' => MetaModel::GetLabel($sClassName, $sAttCode), 'description' => MetaModel::GetDescription($sClassName, $sAttCode));
-			}
+		$iDefaultPageSize =  MetaModel::GetConfig()->GetMinDisplayLimit();
+			
 		}
 		// Load only the requested columns
 		$aAttToLoad = array(); // attributes to load
@@ -1093,83 +860,25 @@ EOF
 		}
 		$oSet->OptimizeColumnLoad($aAttToLoad);
 
-		$aValues = array();
-		$oSet->Seek(0);
+		$iDefaultPageSize =  MetaModel::GetConfig()->GetMinDisplayLimit();
+		$iPageSize = MetaModel::GetConfig()->GetMinDisplayLimit();
+		$sSelectMode = 'none';
+				
+		$sClassAlias = $oSet->GetClassAlias();
+		$oDataTable = new DataTable($iListId, $oSet, $aAuthorizedClasses);
+
+		$oSettings = DataTableSettings::GetDataModelSettings($aAuthorizedClasses, $bViewLink, $aList);
+		
 		$bDisplayLimit = isset($aExtraParams['display_limit']) ? $aExtraParams['display_limit'] : true;
-		$iMaxObjects = -1;
 		if ($bDisplayLimit)
 		{
-			if ($oSet->Count() > MetaModel::GetConfig()->GetMaxDisplayLimit())
-			{
-				$iMaxObjects = MetaModel::GetConfig()->GetMinDisplayLimit();
-			}
-		}
-		while (($aObjects = $oSet->FetchAssoc()) && ($iMaxObjects != 0))
-		{
-			$aRow = array();
-			foreach($aAuthorizedClasses as $sAlias => $sClassName) // TO DO: check if the user has enough rights to view the classes of the list...
-			{
-				if ($bViewLink)
-				{
-					if (is_null($aObjects[$sAlias]))
-					{
-						$aRow['key_'.$sAlias] = '';
-					}
-					else
-					{
-						$aRow['key_'.$sAlias] = $aObjects[$sAlias]->GetHyperLink();
-					}
-				}
-				foreach($aList[$sAlias] as $sAttCode)
-				{
-					if (is_null($aObjects[$sAlias]))
-					{
-						$aRow[$sAttCode.'_'.$sAlias] = '';
-					}
-					else
-					{
-						$aRow[$sAttCode.'_'.$sAlias] = $aObjects[$sAlias]->GetAsHTML($sAttCode);
-					}
-				}
-			}
-			$aValues[] = $aRow;
-			$iMaxObjects--;
-		}
-		$sHtml .= '<table class="listContainer">';
-		$sColspan = '';
-		if ($bDisplayMenu)
-		{
-			$oMenuBlock = new MenuBlock($oSet->GetFilter());
-			$sColspan = 'colspan="2"';
-			$aMenuExtraParams = $aExtraParams;
-			if (!empty($sLinkageAttribute))
-			{
-				$aMenuExtraParams = $aExtraParams;
-			}
-			if ($bDisplayLimit && ($oSet->Count() > MetaModel::GetConfig()->GetMaxDisplayLimit()))
-			{
-				// list truncated
-				$divId = $aExtraParams['block_id'];
-				$sFilter = $oSet->GetFilter()->serialize();
-				$aExtraParams['display_limit'] = false; // To expand the full list
-				$sExtraParams = addslashes(str_replace('"', "'", json_encode($aExtraParams))); // JSON encode, change the style of the quotes and escape them
-				$sHtml .= '<tr class="containerHeader"><td>'.Dict::Format('UI:TruncatedResults', MetaModel::GetConfig()->GetMinDisplayLimit(), $oSet->Count()).'&nbsp;&nbsp;<span style=\"cursor:pointer;\" onClick="Javascript:ReloadTruncatedList(\''.$divId.'\', \''.$sFilter.'\', \''.$sExtraParams.'\');">'.Dict::S('UI:DisplayAll').'</span></td><td>';
-				$oPage->add_ready_script("$('#{$divId} table.listResults').addClass('truncated');");
-				$oPage->add_ready_script("$('#{$divId} table.listResults tr:last td').addClass('truncated');");
-			}
-			else
-			{
-				// Full list
-				$sHtml .= '<tr class="containerHeader"><td>&nbsp;'.Dict::Format('UI:CountOfResults', $oSet->Count()).'</td><td>';
-			}
-			$sHtml .= $oMenuBlock->GetRenderContent($oPage, $aMenuExtraParams, $aMenuExtraParams['currentId']);
-			$sHtml .= '</td></tr>';
+			$iDefaultPageSize = MetaModel::GetConfig()->GetMinDisplayLimit(); //TODO use user's prefs instead if any
+			$oSettings->iDefaultPageSize = $iDefaultPageSize;
 		}
-		$sHtml .= "<tr><td $sColspan>";
-		$sHtml .= $oPage->GetTable($aAttribs, $aValues);
-		$sHtml .= '</td></tr>';
-		$sHtml .= '</table>';
-		return $sHtml;
+		
+		$oSettings->aSortOrder = MetaModel::GetOrderByDefault($sClassName);
+		
+		return $oDataTable->Display($oPage, $oSettings, $bDisplayMenu, $sSelectMode, $bViewLink, $aExtraParams);
 	}
 	
 	static function DisplaySetAsCSV(WebPage $oPage, CMDBObjectSet $oSet, $aParams = array())

+ 769 - 0
application/datatable.class.inc.php

@@ -0,0 +1,769 @@
+<?php
+// Copyright (C) 2012 Combodo SARL
+//
+//   This program is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU General Public License as published by
+//   the Free Software Foundation; version 3 of the License.
+//
+//   This program is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU General Public License for more details.
+//
+//   You should have received a copy of the GNU General Public License
+//   along with this program; if not, write to the Free Software
+//   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+/**
+ * Data Table to display a set of objects in a tabular manner in HTML
+ *
+ * @author      Erwan Taloc <erwan.taloc@combodo.com>
+ * @author      Romain Quetiez <romain.quetiez@combodo.com>
+ * @author      Denis Flaven <denis.flaven@combodo.com>
+ * @license     http://www.opensource.org/licenses/gpl-3.0.html GPL
+ */
+class DataTable
+{
+	protected $iListId;		// Unique ID inside the web page
+	protected $sTableId;	// identifier for sqve the settings (combined with the class aliases)
+	protected $oSet;		// The set of objects to display
+	protected $aClassAliases;	// The aliases (alias => class) inside the set
+	protected $iNbObjects;		// Total number of objects inthe set
+	protected $bUseCustomSettings;	// Whether or not the current display uses custom settings
+	protected $oDefaultSettings;	// the default settings for displaying such a list
+		
+	/**
+	 * @param $iListId mixed Unique ID for this div/table in the page
+	 * @param $oSet DBObjectSet The set of data to display
+	 * @param $aClassAliases Hash The list of classes/aliases to be displayed in this set $sAlias => $sClassName
+	 * @param $sTableId mixed A string (or null) identifying this table in order to persist its settings
+	 */
+	public function __construct($iListId, $oSet, $aClassAliases, $sTableId = null)
+	{
+		$this->iListId = $iListId;
+		$this->oSet = $oSet;
+		$this->aClassAliases = $aClassAliases;
+		$this->sTableId = $sTableId;
+		$this->iNbObjects = $oSet->Count();
+		$this->bUseCustomSettings = false;
+		$this->oDefaultSettings = null;
+	}
+	
+	public function Display(WebPage $oPage, DataTableSettings $oSettings, $bActionsMenu, $sSelectMode, $bViewLink, $aExtraParams)
+	{
+		$this->oDefaultSettings = $oSettings;
+		if ($this->sTableId != null)
+		{
+			// Identified tables can have their own specific settings
+			$oCustomSettings = DataTableSettings::GetTableSettings($this->aClassAliases, $this->sTableId);
+		}
+		else
+		{
+			$oCustomSettings = null;
+		}
+		
+		if ($oCustomSettings != null)
+		{
+			// Custom settings overload the default ones
+			$this->bUseCustomSettings = true;
+		}
+		else
+		{
+			$oCustomSettings = $oSettings;
+		}
+
+		if ($oCustomSettings->iDefaultPageSize != -1)
+		{
+			$this->oSet->SetLimit($oCustomSettings->iDefaultPageSize);
+		}
+		
+		
+		return $this->GetAsHTML($oPage, $oCustomSettings->iDefaultPageSize, $oCustomSettings->iDefaultPageSize, 0, $oCustomSettings->aColumns, $bActionsMenu, true, $sSelectMode, $bViewLink, $aExtraParams);
+	}
+	
+	public function GetAsHTML(WebPage $oPage, $iPageSize, $iDefaultPageSize, $iPageIndex, $aColumns, $bActionsMenu, $bToolkitMenu, $sSelectMode, $bViewLink, $aExtraParams)
+	{
+		$sObjectsCount = $this->GetObjectCount($oPage, $sSelectMode);
+		$sPager = $this->GetPager($oPage, $iPageSize, $iDefaultPageSize, $iPageIndex);
+		$sActionsMenu = '';
+		$sToolkitMenu = '';
+		if ($bActionsMenu)
+		{
+			$sActionsMenu = $this->GetActionsMenu($oPage, $aExtraParams);
+		}
+		if ($bToolkitMenu)
+		{
+			$sToolkitMenu = $this->GetToolkitMenu($oPage, $aExtraParams);
+		}
+		$sDataTable = $this->GetHTMLTable($oPage, $aColumns, $sSelectMode, $iPageSize, $bViewLink, $aExtraParams);
+		$sConfigDlg = $this->GetTableConfigDlg($oPage, $aColumns, $bViewLink);
+		
+		$sHtml = "<table id=\"datatable_{$this->iListId}\" class=\"datatable\">\n";
+		$sHtml .= "<tr><td>$sObjectsCount</td><td class=\"menucontainer\">$sActionsMenu</td></tr>\n";
+		$sHtml .= "<tr><td>$sPager</td><td class=\"menucontainer\">$sToolkitMenu</td></tr>\n";
+		$sHtml .= "<tr><td class=\"datacontents\" colspan=\"2\">$sDataTable</td></tr>\n";
+		$sHtml .= "</table>\n";
+		$sHtml .= $sConfigDlg;
+		
+		$aOptions = array(
+			'sPersistentId' => '',
+			'sFilter' => $this->oSet->GetFilter()->serialize(),
+			'oColumns' => $aColumns,
+			'sSelectMode' => $sSelectMode,
+			'sViewLink' => ($bViewLink ? 'true' : 'false'),
+			'iNbObjects' => $this->iNbObjects,
+			'iDefaultPageSize' => $iDefaultPageSize,
+			'iPageSize' =>  $iPageSize,
+			'iPageIndex' =>  $iPageIndex,
+			'oClassAliases' => $this->aClassAliases,
+			'sTableId' => $this->sTableId,
+			'oExtraParams' => $aExtraParams,
+			'sRenderUrl' => utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php',
+			'oRenderParameters' => array('str' => ''), // Forces JSON to encode this as a object...
+			'oDefaultSettings' => array('str' => ''), // Forces JSON to encode this as a object...
+			'oLabels' => array('moveup' => Dict::S('UI:Button:MoveUp'), 'movedown' => Dict::S('UI:Button:MoveDown')),
+		);
+		if($this->oDefaultSettings != null)
+		{
+			$aOptions['oDefaultSettings'] = $this->GetAsHash($this->oDefaultSettings);
+		}
+		$sJSOptions = json_encode($aOptions);
+		$oPage->add_ready_script("$('#datatable_{$this->iListId}').datatable($sJSOptions);");
+		return $sHtml;
+	}
+	
+	/**
+	 * When refreshing the body of a paginated table, get the rows of the table (inside the TBODY)
+	 * return string The HTML rows to insert inside the <tbody> node
+	 */
+	public function GetAsHTMLTableRows(WebPage $oPage, $iPageSize, $aColumns, $sSelectMode, $bViewLink, $aExtraParams)
+	{
+		$aAttribs = $this->GetHTMLTableConfig($aColumns, $sSelectMode, $bViewLink);
+		$aValues = $this->GetHTMLTableValues($aColumns, $sSelectMode, $iPageSize, $bViewLink, $aExtraParams);
+		
+		$sHtml = '';
+		foreach($aValues as $aRow)
+		{
+			$sHtml .= $oPage->GetTableRow($aRow, $aAttribs);
+		}
+		return $sHtml;
+	}
+	
+	protected function GetObjectCount(WebPage $oPage, $sSelectMode)
+	{
+		if (($sSelectMode == 'single') || ($sSelectMode == 'multiple'))
+		{
+			$sHtml = Dict::Format('UI:Pagination:HeaderSelection', '<span id="total">'.$this->iNbObjects.'</span>', '<span class="selectedCount">0</span>');
+		}
+		else
+		{
+			$sHtml = Dict::Format('UI:Pagination:HeaderNoSelection', '<span id="total">'.$this->iNbObjects.'</span>');
+		}
+		return $sHtml;		
+	}
+	protected function GetPager(WebPage $oPage, $iPageSize, $iDefaultPageSize, $iPageIndex)
+	{
+		$sHtml = '';
+		if ($iPageSize == -1) // Display all
+		{
+			$sPagerStyle = 'style="display:none"'; // no limit: display the full table, so hide the "pager" UI
+		}
+		else
+		{
+			$sPagerStyle = '';
+		}
+		
+		$sCombo = '<select class="pagesize">';
+		for($iPage = 1; $iPage < 5; $iPage++)
+		{
+			$sSelected = ($iPage == $iPageSize) ? 'selected="selected"' : '';
+			$iNbItems = $iPage * $iDefaultPageSize;
+			$sCombo .= "<option  $sSelected value=\"$iNbItems\">$iNbItems</option>";
+		}
+		$sSelected = (-1 == $iPageSize) ? 'selected="selected"' : '';
+		$sCombo .= "<option  $sSelected value=\"-1\">".Dict::S('UI:Pagination:All')."</option>";
+		$sCombo .= '</select>';
+		
+		$sPages = Dict::S('UI:Pagination:PagesLabel');
+		$sPageSizeCombo = Dict::Format('UI:Pagination:PageSize', $sCombo);
+		
+		$iNbPages = ($iPageSize == -1) ? 1 : ceil($this->iNbObjects / $iPageSize);
+		$aPagesToDisplay = array();
+		for($idx = 0; $idx <= min(4, $iNbPages-1); $idx++)
+		{
+			if ($idx == 0)
+			{
+				$aPagesToDisplay[$idx] = '<span page="0" class="curr_page">1</span>';
+			}
+			else
+			{
+				$aPagesToDisplay[$idx] = "<span id=\"gotopage_$idx\" class=\"gotopage\" page=\"$idx\">".(1+$idx)."</span>";
+			}
+		}
+		$iLastPageIdx = $iNbPages - 1;
+		if (!isset($aPagesToDisplay[$iLastPageIdx]))
+		{
+			unset($aPagesToDisplay[$idx - 1]); // remove the last page added to make room for the very last page
+			$aPagesToDisplay[$iLastPageIdx] = "<span id=\"gotopage_$iLastPageIdx\" class=\"gotopage\" page=\"$iLastPageIdx\">... $iNbPages</span>";
+		}
+		$sPagesLinks = implode('', $aPagesToDisplay);
+		$sPagesList = '['.implode(',', array_keys($aPagesToDisplay)).']';
+
+		$sSelectionMode = ($iNbPages == 1) ? '' : 'positive';
+		$sHtml =
+<<<EOF
+<div id="pager{$this->iListId}" class="pager" $sPagerStyle>
+		<p><table class="pagination"><tr><td>$sPages</td><td><img src="../images/first.png" class="first"/></td>
+		<td><img src="../images/prev.png" class="prev"/></td>
+		<td><span id="index">$sPagesLinks</span></td>
+		<td><img src="../images/next.png" class="next"/></td>
+		<td><img src="../images/last.png" class="last"/></td>
+		<td>$sPageSizeCombo</td>
+		<td><span id="loading">&nbsp;</span></td>
+		</tr>
+		</table>
+		
+		<input type="hidden" name="selectionMode" value="$sSelectionMode"></input>
+</div>
+EOF;
+		return $sHtml;
+	}
+	
+	protected function GetActionsMenu(WebPage $oPage, $aExtraParams)
+	{
+		$oMenuBlock = new MenuBlock($this->oSet->GetFilter());
+		
+		$sHtml = $oMenuBlock->GetRenderContent($oPage, $aExtraParams, $this->iListId);
+		return $sHtml;
+	}
+	
+	protected function GetToolkitMenu(WebPage $oPage, $aExtraParams)
+	{
+		$sMenuTitle = Dict::S('UI:ConfigureThisList');
+		$sHtml = '<div class="itop_popup" id="tk_'.$this->iListId.'"><ul><li><img src="../images/toolkit_menu.png"><ul><li><a  onclick="$(\'#datatable_dlg_'.$this->iListId.'\').dialog(\'open\');">'.$sMenuTitle.'</a></li></li></ul></div>';
+		//$oPage->add_ready_script("$('#tk_{$this->iListId} > ul').popupmenu();");
+		return $sHtml;
+	}
+	
+	protected function GetTableConfigDlg(WebPage $oPage, $aColumns, $bViewLink)
+	{
+		$sHtml = "<div id=\"datatable_dlg_{$this->iListId}\" style=\"display: none;\">";
+		$sHtml .= "<form onsubmit=\"return false\">";
+		$sChecked = ($this->bUseCustomSettings) ? '' : 'checked';
+		$sHtml .= "<p><input type=\"radio\" name=\"settings\" $sChecked value=\"defaults\">&nbsp;".Dict::S('UI:UseDefaultSettings').'</p>';
+		$sHtml .= "<fieldset>";
+		$sChecked = ($this->bUseCustomSettings) ? 'checked':  '';
+		$sHtml .= "<legend class=\"transparent\"><input type=\"radio\" class=\"specific_settings\" name=\"settings\" $sChecked value=\"specific\">&nbsp;".Dict::S('UI:UseSpecificSettings')."</legend>";
+		$sHtml .= Dict::S('UI:ColumnsAndSortOrder').'<br/><ul class="sortable_field_list" id="sfl_'.$this->iListId.'"></ul>';
+		
+		$sHtml .= '<p>'.Dict::Format('UI:Display_X_ItemsPerPage', '<input type="text" size="4" name="page_size" value="10">').'</p>';
+		$sHtml .= "</fieldset>";
+		$sHtml .= "<fieldset>";
+		$sSaveChecked = ($this->sTableId != null) ? 'checked' : '';
+		$sCustomChecked = ($this->sTableId != null) ? 'checked' : '';
+		$sGenericChecked = ($this->sTableId == null) ? 'checked' : '';
+		$sHtml .= "<legend class=\"transparent\"><input type=\"checkbox\" $sSaveChecked name=\"save_settings\">&nbsp;".Dict::S('UI:UseSavetheSettings')."</legend>";
+		$sHtml .= '<p><input type="radio" name="scope" '.$sCustomChecked.' value="this_list">&nbsp;'.Dict::S('UI:OnlyForThisList').'&nbsp;&nbsp;&nbsp;&nbsp;';
+		$sHtml .= '<input type="radio" name="scope" '.$sGenericChecked.' value="defaults">&nbsp;'.Dict::S('UI:ForAllLists').'</p>';
+		$sHtml .= "</fieldset>";
+		$sHtml .= '<table style="width:100%"><tr><td style="text-align:center;">';
+		$sHtml .= '<button type="button" onclick="$(\'#datatable_'.$this->iListId.'\').datatable(\'onDlgCancel\'); $(\'#datatable_dlg_'.$this->iListId.'\').dialog(\'close\')">'.Dict::S('UI:Button:Cancel').'</button>';
+		$sHtml .= '</td><td style="text-align:center;">';
+		$sHtml .= '<button type="submit" onclick="$(\'#datatable_'.$this->iListId.'\').datatable(\'onDlgOk\');$(\'#datatable_dlg_'.$this->iListId.'\').dialog(\'close\');">'.Dict::S('UI:Button:Ok').'</button>';
+		$sHtml .= '</td></tr></table>';
+		$sHtml .= "</form>";
+		$sHtml .= "</div>";
+		
+		$sDlgTitle = addslashes(Dict::S('UI:ListConfigurationTitle'));
+		$oPage->add_ready_script("$('#datatable_dlg_{$this->iListId}').dialog({autoOpen: false, title: '$sDlgTitle', width: 500, close: function() { $('#datatable_{$this->iListId}').datatable('onDlgCancel'); } });");
+
+		return $sHtml;
+	}
+	
+	public function GetAsHash($oSetting)
+	{
+		$aSettings = array('iDefaultPageSize' => $oSetting->iDefaultPageSize, 'oColumns' => $oSetting->aColumns);
+		return $aSettings;
+	}
+	
+	protected function GetHTMLTableConfig($aColumns, $sSelectMode, $bViewLink)
+	{
+		$aAttribs = array();
+		if ($sSelectMode == 'multiple')
+		{
+			$aAttribs['form::select'] = array('label' => "<input type=\"checkbox\" onClick=\"CheckAll('.selectList{$this->iListId}:not(:disabled)', this.checked);\" class=\"checkAll\"></input>", 'description' => Dict::S('UI:SelectAllToggle+'));
+		}
+		else if ($sSelectMode == 'single')
+		{
+			$aAttribs['form::select'] = array('label' => "", 'description' => '');
+		}
+
+		foreach($this->aClassAliases as $sAlias => $sClassName)
+		{
+			foreach($aColumns[$sAlias] as $sAttCode => $aData)
+			{
+				if ($aData['checked'])
+				{
+					if ($sAttCode == '_key_')
+					{
+						$aAttribs['key_'.$sAlias] = array('label' => MetaModel::GetName($sClassName), 'description' => '');
+					}
+					else
+					{
+						$oAttDef = MetaModel::GetAttributeDef($sClassName, $sAttCode);
+						$aAttribs[$sAttCode.'_'.$sAlias] = array('label' => MetaModel::GetLabel($sClassName, $sAttCode), 'description' => $oAttDef->GetOrderByHint());
+					}
+				}
+			}
+		}
+		return $aAttribs;
+	}
+	
+	protected function GetHTMLTableValues($aColumns, $sSelectMode, $iPageSize, $bViewLink, $aExtraParams)
+	{
+		$aValues = array();
+		$this->oSet->Seek(0);
+		$iMaxObjects = $iPageSize;
+		while (($aObjects = $this->oSet->FetchAssoc()) && ($iMaxObjects != 0))
+		{
+			$bFirstObject = true;
+			$aRow = array();
+			foreach($this->aClassAliases as $sAlias => $sClassName)
+			{
+				$sHilightClass = $aObjects[$sAlias]->GetHilightClass();
+				if ($sHilightClass != '')
+				{
+					$aRow['@class'] = $sHilightClass;	
+				}
+				if ((($sSelectMode == 'single') || ($sSelectMode == 'multiple')) && $bFirstObject)
+				{
+					if (array_key_exists('selection_enabled', $aExtraParams) && isset($aExtraParams['selection_enabled'][$aObjects[$sAlias]->GetKey()]))
+					{
+						$sDisabled = ($aExtraParams['selection_enabled'][$aObjects[$sAlias]->GetKey()]) ? '' : ' disabled="disabled"';
+					}
+					else
+					{
+						$sDisabled = '';
+					}
+					if ($sSelectMode == 'single')
+					{
+						$aRow['form::select'] = "<input type=\"radio\" $sDisabled class=\"selectList{$this->iListId}\" name=\"selectObject\" value=\"".$aObjects[$sAlias]->GetKey()."\"></input>";
+					}
+					else
+					{
+						$aRow['form::select'] = "<input type=\"checkBox\" $sDisabled class=\"selectList{$this->iListId}\" name=\"selectObject[]\" value=\"".$aObjects[$sAlias]->GetKey()."\"></input>";
+					}
+				}
+				foreach($aColumns[$sAlias] as $sAttCode => $aData)
+				{
+					if ($aData['checked'])
+					{
+						if ($sAttCode == '_key_')
+						{
+							$aRow['key_'.$sAlias] = $aObjects[$sAlias]->GetHyperLink();
+						}
+						else
+						{
+							$aRow[$sAttCode.'_'.$sAlias] = $aObjects[$sAlias]->GetAsHTML($sAttCode);
+						}
+					}
+				}
+				$bFirstObject = false;
+			}
+			$aValues[] = $aRow;
+			$iMaxObjects--;
+		}
+		return $aValues;
+	}
+	
+	public function GetHTMLTable(WebPage $oPage, $aColumns, $sSelectMode, $iPageSize, $bViewLink, $aExtraParams)
+	{
+		$iNbPages = ($iPageSize == -1) ? 1 : ceil($this->iNbObjects / $iPageSize);
+		$aAttribs = $this->GetHTMLTableConfig($aColumns, $sSelectMode, $bViewLink);
+
+		$aValues = $this->GetHTMLTableValues($aColumns, $sSelectMode, $iPageSize, $bViewLink, $aExtraParams);
+
+		$sHtml = '<table class="listContainer">';
+
+		foreach($this->oSet->GetFilter()->GetInternalParams() as $sName => $sValue)
+		{
+			$aExtraParams['query_params'][$sName] = $sValue;
+		}
+
+		$sHtml .= "<tr><td>";
+		$sHtml .= $oPage->GetTable($aAttribs, $aValues);
+		$sHtml .= '</td></tr>';
+		$sHtml .= '</table>';
+		$iCount = $this->iNbObjects;
+
+		$aArgs = $this->oSet->GetArgs();
+		$sExtraParams = addslashes(str_replace('"', "'", json_encode(array_merge($aExtraParams, $aArgs)))); // JSON encode, change the style of the quotes and escape them
+		$sSelectModeJS = '';
+		$sHeaders = '';
+		if (($sSelectMode == 'single') || ($sSelectMode == 'multiple'))
+		{
+			$sSelectModeJS = $sSelectMode;
+			$sHeaders = 'headers: { 0: {sorter: false}},';
+		}
+		$sDisplayKey = ($bViewLink) ? 'true' : 'false';
+		// Protect against duplicate elements in the Zlist
+		$aUniqueOrderedList = array();
+		foreach($this->aClassAliases as $sAlias => $sClassName)
+		{
+			foreach($aColumns[$sAlias] as $sAttCode => $aData)
+			{
+				if ($aData['checked'])
+				{
+					$aUniqueOrderedList[$sAttCode] = true;
+				}
+			}
+		}
+		$aUniqueOrderedList = array_keys($aUniqueOrderedList);
+		$sJSColumns = json_encode($aColumns);
+		$sJSClassAliases = json_encode($this->aClassAliases);
+		$sCssCount = isset($aExtraParams['cssCount']) ? ", cssCount: '{$aExtraParams['cssCount']}'" : '';
+		$this->oSet->ApplyParameters();
+		// Display the actual sort order of the table
+		$aRealSortOrder = $this->oSet->GetRealSortOrder();
+		$aDefaultSort = array();
+		$iColOffset = 0;
+		if (($sSelectMode == 'single') || ($sSelectMode == 'multiple'))
+		{
+			$iColOffset += 1;
+		}
+		if ($bViewLink)
+		{
+			$iColOffset += 1;
+		}
+		foreach($aRealSortOrder as $sColCode => $bAscending)
+		{
+			$iPos = array_search($sColCode, $aUniqueOrderedList);
+			if ($iPos !== false)
+			{
+				$aDefaultSort[] = "[".($iColOffset+$iPos).",".($bAscending ? '0' : '1')."]";
+			}
+			else if($sColCode == 'friendlyname' && $bViewLink)
+			{
+				$aDefaultSort[] = "[".($iColOffset-1).",".($bAscending ? '0' : '1')."]";
+			}
+		}
+		$sSortList = '';
+		if (count($aDefaultSort) > 0)
+		{
+			$sSortList = ', sortList: ['.implode(',', $aDefaultSort).']';
+		}
+		$sOQL = addslashes($this->oSet->GetFilter()->serialize());
+		$oPage->add_ready_script(
+<<<EOF
+var oTable = $('#{$this->iListId} table.listResults');
+oTable.tablesorter( { $sHeaders widgets: ['myZebra', 'truncatedList'] $sSortList} ).tablesorterPager({container: $('#pager{$this->iListId}'), totalRows:$iCount, size: $iPageSize, filter: '$sOQL', extra_params: '$sExtraParams', select_mode: '$sSelectModeJS', displayKey: $sDisplayKey, columns: $sJSColumns, class_aliases: $sJSClassAliases $sCssCount});
+EOF
+		);
+		
+		//if ($iNbPages == 1)
+		if (false)
+		{
+			if (isset($aExtraParams['cssCount']))
+			{
+				$sCssCount = $aExtraParams['cssCount'];
+				if ($sSelectMode == 'single')
+				{
+					$sSelectSelector = ":radio[name^=selectObj]";
+				}
+				else if ($sSelectMode == 'multiple')
+				{
+					$sSelectSelector = ":checkbox[name^=selectObj]";
+				}
+				$oPage->add_ready_script(
+<<<EOF
+	$('#{$this->iListId} table.listResults $sSelectSelector').change(function() {
+		var c = $('{$sCssCount}');							
+		var v = $('#{$this->iListId} table.listResults $sSelectSelector:checked').length;
+		c.val(v);
+		$('#{$this->iListId} .selectedCount').text(v);
+		c.trigger('change');	
+	});
+EOF
+				);
+			}
+		}
+		return $sHtml;
+	}
+	
+	public function UpdatePager(WebPage $oPage, $iDefaultPageSize, $iStart)
+	{
+		$iPageSize = $iDefaultPageSize;
+		$iPageIndex = 1 + floor($iStart / $iPageSize);
+		$sHtml = $this->GetPager($oPage, $iPageSize, $iDefaultPageSize, $iPageIndex);
+		$oPage->add_ready_script("$('#pager{$this->iListId}').html('".str_replace("\n", ' ', addslashes($sHtml))."');");
+	}
+}
+
+class DataTableSettings implements Serializable
+{
+	public $aClassAliases;
+	public $sTableId;
+	public $iDefaultPageSize;
+	public $aColumns;
+	
+	public function __construct($aClassAliases, $sTableId = null)
+	{
+		$this->aClassAliases = $aClassAliases;
+		$this->sTableId = $sTableId;
+		$this->iDefaultPageSize = 10;
+		$this->aColumns = array();		
+	}
+	
+	protected function Init($iDefaultPageSize, $aSortOrder, $aColumns)
+	{
+		$this->iDefaultPageSize = $iDefaultPageSize;
+		$this->aColumns = $aColumns;
+		$this->FixVisibleColumns();		
+	}
+	
+	public function serialize()
+	{
+		// Save only the 'visible' columns
+		$aColumns = array();
+		foreach($this->aClassAliases as $sAlias => $sClass)
+		{
+			$aColumns[$sAlias] = array();
+			foreach($this->aColumns[$sAlias] as $sAttCode => $aData)
+			{
+				unset($aData['label']); // Don't save the display name
+				unset($aData['alias']); // Don't save the alias (redundant)
+				unset($aData['code']); // Don't save the code (redundant)
+				if ($aData['checked'])
+				{
+					$aColumns[$sAlias][$sAttCode] = $aData;
+				}
+			}
+		}
+		return serialize(
+			array(
+				'iDefaultPageSize' => $this->iDefaultPageSize,
+				'aColumns' => $aColumns,		
+			)
+		);
+	}
+	
+	public function unserialize($sData)
+	{
+		$aData = unserialize($sData);
+		$this->iDefaultPageSize = $aData['iDefaultPageSize'];
+		$this->aColumns = $aData['aColumns'];
+		foreach($this->aClassAliases as $sAlias => $sClass)
+		{
+			foreach($this->aColumns[$sAlias] as $sAttCode => $aData)
+			{
+				$aFieldData = false;
+				if ($sAttCode == '_key_')
+				{
+					$aFieldData = $this->GetFieldData($sAlias, $sAttCode, null, true /* bChecked */, $aData['sort']);
+				}
+				else if (MetaModel::isValidAttCode($sClass, $sAttCode))
+				{
+					$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
+					$aFieldData = $this->GetFieldData($sAlias, $sAttCode, $oAttDef, true /* bChecked */, $aData['sort']);
+				}
+
+				if ($aFieldData)
+				{
+					$this->aColumns[$sAlias][$sAttCode] = $aFieldData;
+				}
+				else
+				{
+					unset($this->aColumns[$sAlias][$sAttCode]);
+				}
+			}
+		}
+		$this->FixVisibleColumns();		
+	}
+	
+	static public function GetDataModelSettings($aClassAliases, $bViewLink, $aDefaultLists)
+	{
+		$oSettings = new DataTableSettings($aClassAliases);
+		// Retrieve the class specific settings for each class/alias based on the 'list' ZList
+		//TODO let the caller pass some other default settings (another Zlist, extre fields...)
+		$aColumns = array();
+		foreach($aClassAliases as $sAlias => $sClass)
+		{
+			if ($aDefaultLists == null)
+			{
+				$aList = cmdbAbstract::FlattenZList(MetaModel::GetZListItems($sClass, 'list'));
+			}
+			else
+			{
+				$aList = $aDefaultLists[$sAlias];
+			}
+			
+			$aSortOrder = MetaModel::GetOrderByDefault($sClass);
+			if ($bViewLink)
+			{
+				$sSort = 'none';
+				if(array_key_exists('friendlyname', $aSortOrder))
+				{
+					$sSort = $aSortOrder['friendlyname'] ? 'asc' : 'desc';
+				}
+				$aColumns[$sAlias]['_key_'] = $oSettings->GetFieldData($sAlias, '_key_', null, true /* bChecked */, $sSort);
+			}
+			foreach($aList as $sAttCode)
+			{
+				$sSort = 'none';
+				if(array_key_exists($sAttCode, $aSortOrder))
+				{
+					$sSort = $aSortOrder[$sAttCode] ? 'asc' : 'desc';
+				}
+				$oAttDef = Metamodel::GetAttributeDef($sClass, $sAttCode);
+				$aFieldData = $oSettings->GetFieldData($sAlias, $sAttCode, $oAttDef, true /* bChecked */, $sSort);
+				if ($aFieldData) $aColumns[$sAlias][$sAttCode] = $aFieldData;
+			}
+		}
+		// TODO retrieve the user default page size or the system wide setting
+		$iDefaultPageSize = MetaModel::GetConfig()->GetMinDisplayLimit();
+		$oSettings->Init($iDefaultPageSize, $aSortOrder, $aColumns);
+		return $oSettings;
+	}
+	
+	protected function FixVisibleColumns()
+	{
+		foreach($this->aClassAliases as $sAlias => $sClass)
+		{
+			foreach($this->aColumns[$sAlias] as $sAttCode => $aData)
+			{
+				// Remove non-existent columns
+				// TODO: check if the existing ones are still valid (in case their type changed)
+				if (($sAttCode != '_key_') && (!MetaModel::IsValidAttCode($sClass, $sAttCode)))
+				{
+					unset($this->aColumns[$sAlias][$sAttCode]);
+				}
+			}
+			$aList = MetaModel::ListAttributeDefs($sClass);
+			
+			// Add the other (non visible ones)
+			foreach($aList as $sAttCode => $oAttDef)
+			{
+				if ( (!array_key_exists($sAttCode, $this->aColumns[$sAlias])) && (!$oAttDef instanceof AttributeLinkSet))
+				{
+					$aFieldData = $this->GetFieldData($sAlias, $sAttCode, $oAttDef, false /* bChecked */, 'none');
+					if ($aFieldData) $this->aColumns[$sAlias][$sAttCode] = $aFieldData;
+				}
+			}
+		}		
+	}
+	
+	static public function GetTableSettings($aClassAliases, $sTableId = null)
+	{
+		$pref = null;
+		$oSettings = new DataTableSettings($aClassAliases, $sTableId);
+		if ($sTableId != null)
+		{
+			// An identified table, let's fetch its own settings (if any)
+			$pref = appUserPreferences::GetPref($oSettings->GetPrefsKey($sTableId), null);
+		}
+		
+		if ($pref == null)
+		{
+			// Try the global preferred values for this class / set of classes
+			$pref = appUserPreferences::GetPref($oSettings->GetPrefsKey(null), null);
+			if ($pref == null)
+			{
+				// no such settings, use the default values provided by the data model
+				return null;
+			}
+		}
+		$oSettings->unserialize($pref);
+		return $oSettings;
+	}
+	
+	public function Save()
+	{
+		if ($this->sTableId == null) return false; // Cannot save, the table is not identified, use SaveAsDefault instead
+		
+		$sSettings = $this->serialize();
+		appUserPreferences::SetPref($this->GetPrefsKey($this->sTableId), $sSettings);
+		return true;
+	}
+
+	public function SaveAsDefault()
+	{
+		$sSettings = $this->serialize();
+		appUserPreferences::SetPref($this->GetPrefsKey(null), $sSettings);
+		return true;
+	}
+	
+
+	/**
+	 * Clear the preferences for this particular table
+	 * @param $bResetAll boolean If true,the settings for all tables of the same class(es)/alias(es) are reset
+	 */
+	public function ResetToDefault($bResetAll)
+	{
+		if (($this->sTableId == null) && (!$bResetAll)) return false; // Cannot reset, the table is not identified, use force $bResetAll instead
+		if ($bResetAll)
+		{
+			// Turn the key into a suitable PCRE pattern
+			$sKey = $this->GetPrefsKey(null);
+			$sPattern = '!^'.str_replace(array('*'), array('.*'), $sKey).'$!';
+			appUserPreferences::UnsetPref($sPattern, true);
+		}
+		else
+		{
+			appUserPreferences::UnsetPref($this->GetPrefsKey($this->sTableId), false);
+		}
+		return true;
+	}
+	
+	protected function GetPrefsKey($sTableId = null)
+	{
+		if ($sTableId == null) $sTableId = '*';
+		$aKeys = array();
+		foreach($this->aClassAliases as $sAlias => $sClass)
+		{
+			$aKeys[] = $sAlias.'-'.$sClass;
+		}
+		return implode('/', $aKeys).'|'.$sTableId;
+	}
+	
+	protected function GetFieldData($sAlias, $sAttCode, $oAttDef, $bChecked, $sSort)
+	{
+		$ret = false;
+		if ($sAttCode == '_key_')
+		{
+			$sLabel = Dict::Format('UI:ExtKey_AsLink', MetaModel::GetName($this->aClassAliases[$sAlias]));
+			$ret = array(
+				'label' => $sLabel,
+				'checked' => true,
+				'disabled' => true,
+				'alias' => $sAlias,
+				'code' => $sAttCode,
+				'sort' => $sSort,
+			);
+		}
+		else if (!$oAttDef->IsLinkSet())
+		{
+			$sLabel = $oAttDef->GetLabel();
+			if ($oAttDef->IsExternalKey())
+			{
+				$sLabel = Dict::Format('UI:ExtKey_AsLink', $oAttDef->GetLabel());
+			}
+			else if ($oAttDef->IsExternalField())
+			{
+				$oExtAttDef = $oAttDef->GetExtAttDef();
+				$sLabel = Dict::Format('UI:ExtField_AsRemoteField', $oAttDef->GetLabel(), $oExtAttDef->GetLabel());
+			}
+			elseif ($oAttDef instanceof AttributeFriendlyName)
+			{
+				$sLabel = Dict::Format('UI:ExtKey_AsFriendlyName', $oAttDef->GetLabel());
+			}
+			$ret = array(
+				'label' => $sLabel,
+				'checked' => $bChecked,
+				'disabled' => false,
+				'alias' => $sAlias,
+				'code' => $sAttCode,
+				'sort' => $sSort,
+			);
+		}
+		return $ret;		
+	}
+}

+ 2 - 2
application/displayblock.class.inc.php

@@ -1332,11 +1332,11 @@ class MenuBlock extends DisplayBlock
 		
 		if (count($aFavoriteActions) > 0)
 		{
-			$sHtml .= "<div class=\"itop_popup\"><ul>\n<li>".Dict::S('UI:Menu:OtherActions')."\n<ul>\n";
+			$sHtml .= "<div class=\"itop_popup actions_menu\"><ul>\n<li>".Dict::S('UI:Menu:OtherActions')."\n<ul>\n";
 		}
 		else
 		{
-			$sHtml .= "<div class=\"itop_popup\"><ul>\n<li>".Dict::S('UI:Menu:Actions')."\n<ul>\n";
+			$sHtml .= "<div class=\"itop_popup actions_menu\"><ul>\n<li>".Dict::S('UI:Menu:Actions')."\n<ul>\n";
 		}	
 		$sPrevUrl = '';
 		foreach ($aActions as $key => $aAction)

+ 2 - 0
application/itopwebpage.class.inc.php

@@ -77,6 +77,8 @@ class iTopWebPage extends NiceWebPage
 		$this->add_linked_script('../js/g.pie.js');
 		$this->add_linked_script('../js/g.dot.js');
 		$this->add_linked_script('../js/charts.js');
+		$this->add_linked_script('../js/field_sorter.js');
+		$this->add_linked_script('../js/datatable.js');
 		
 		$this->m_sInitScript =
 <<< EOF

+ 36 - 0
application/user.preferences.class.inc.php

@@ -78,6 +78,42 @@ class appUserPreferences extends DBObject
 	}
 	
 	/**
+	 * Clears the value for a given preference (or list of preferences that matches a pattern), and updates the database
+	 * @param string $sPattern Code/Pattern of the properties/preferences to reset
+	 * @param boolean $bPattern Whether or not the supplied code is a PCRE pattern
+	 */
+	static function UnsetPref($sCodeOrPattern, $bPattern = false)
+	{
+		if (self::$oUserPrefs == null)
+		{
+			self::Load();
+		}
+		$aPrefs = self::$oUserPrefs->Get('preferences');
+		if ($bPattern)
+		{
+			// the supplied code is a pattern, clear all preferences that match
+			foreach($aPrefs as $sKey => $void)
+			{
+				if (preg_match($sCodeOrPattern, $sKey))
+				{
+					unset($aPrefs[$sKey]);
+				}
+			}
+			self::$oUserPrefs->Set('preferences', $aPrefs);
+		}
+		else
+		{
+			unset($aPrefs[$sCode]);
+			self::$oUserPrefs->Set('preferences', $aPrefs);
+		}
+		// Save only if needed
+		if (self::$oUserPrefs->IsModified())
+		{
+			self::Save();
+		}
+	}
+	
+	/**
 	 * Call this function to get all the preferences for the user, packed as a JSON object
 	 * @return string JSON representation of the preferences
 	 */