diff --git a/application/dashboard.class.inc.php b/application/dashboard.class.inc.php index 93f6c63d4..04f9dbe31 100644 --- a/application/dashboard.class.inc.php +++ b/application/dashboard.class.inc.php @@ -34,15 +34,16 @@ abstract class Dashboard protected $oDOMNode; protected $sId; protected $aCells; + protected $oMetaModel; public function __construct($sId) { - $this->sLayoutClass = null; + $this->sLayoutClass = 'DashboardLayoutOneCol'; $this->aCells = array(); $this->oDOMNode = null; $this->sId = $sId; } - + public function FromXml($sXml) { $this->aCells = array(); // reset the content of the dashboard @@ -50,60 +51,83 @@ abstract class Dashboard $oDoc = new DOMDocument(); $oDoc->loadXML($sXml); restore_error_handler(); - $this->oDOMNode = $oDoc->getElementsByTagName('dashboard')->item(0); - - $oLayoutNode = $this->oDOMNode->getElementsByTagName('layout')->item(0); - $this->sLayoutClass = $oLayoutNode->textContent; - - $oTitleNode = $this->oDOMNode->getElementsByTagName('title')->item(0); - $this->sTitle = $oTitleNode->textContent; - - $oCellsNode = $this->oDOMNode->getElementsByTagName('cells')->item(0); - $oCellsList = $oCellsNode->getElementsByTagName('cell'); - $aCellOrder = array(); - $iCellRank = 0; - foreach($oCellsList as $oCellNode) - { - $aDashletList = array(); - $oCellRank = $oCellNode->getElementsByTagName('rank')->item(0); - if ($oCellRank) - { - $iCellRank = (float)$oCellRank->textContent; - } - $oDashletsNode = $oCellNode->getElementsByTagName('dashlets')->item(0); - $oDashletList = $oDashletsNode->getElementsByTagName('dashlet'); - $iRank = 0; - $aDashletOrder = array(); - foreach($oDashletList as $oDomNode) - { - $sDashletClass = $oDomNode->getAttribute('xsi:type'); - $oRank = $oDomNode->getElementsByTagName('rank')->item(0); - if ($oRank) - { - $iRank = (float)$oRank->textContent; - } - $sId = $oDomNode->getAttribute('id'); - $oNewDashlet = new $sDashletClass(new ModelReflectionRuntime(), $sId); - $oNewDashlet->FromDOMNode($oDomNode); - $aDashletOrder[] = array('rank' => $iRank, 'dashlet' => $oNewDashlet); - } - usort($aDashletOrder, array(get_class($this), 'SortOnRank')); - $aDashletList = array(); - foreach($aDashletOrder as $aItem) - { - $aDashletList[] = $aItem['dashlet']; - } - $aCellOrder[] = array('rank' => $iCellRank, 'dashlets' => $aDashletList); - } - usort($aCellOrder, array(get_class($this), 'SortOnRank')); - foreach($aCellOrder as $aItem) - { - $this->aCells[] = $aItem['dashlets']; - } - - + $this->FromDOMDocument($oDoc); } + public function FromDOMDocument(DOMDocument $oDoc) + { + $this->oDOMNode = $oDoc->getElementsByTagName('dashboard')->item(0); + + if ($oLayoutNode = $this->oDOMNode->getElementsByTagName('layout')->item(0)) + { + $this->sLayoutClass = $oLayoutNode->textContent; + } + else + { + $this->sLayoutClass = 'DashboardLayoutOneCol'; + } + + if ($oTitleNode = $this->oDOMNode->getElementsByTagName('title')->item(0)) + { + $this->sTitle = $oTitleNode->textContent; + } + else + { + $this->sTitle = ''; + } + + if ($oCellsNode = $this->oDOMNode->getElementsByTagName('cells')->item(0)) + { + $oCellsList = $oCellsNode->getElementsByTagName('cell'); + $aCellOrder = array(); + $iCellRank = 0; + foreach($oCellsList as $oCellNode) + { + $aDashletList = array(); + $oCellRank = $oCellNode->getElementsByTagName('rank')->item(0); + if ($oCellRank) + { + $iCellRank = (float)$oCellRank->textContent; + } + $oDashletsNode = $oCellNode->getElementsByTagName('dashlets')->item(0); + { + $oDashletList = $oDashletsNode->getElementsByTagName('dashlet'); + $iRank = 0; + $aDashletOrder = array(); + foreach($oDashletList as $oDomNode) + { + $sDashletClass = $oDomNode->getAttribute('xsi:type'); + $oRank = $oDomNode->getElementsByTagName('rank')->item(0); + if ($oRank) + { + $iRank = (float)$oRank->textContent; + } + $sId = $oDomNode->getAttribute('id'); + $oNewDashlet = new $sDashletClass($this->oMetaModel, $sId); + $oNewDashlet->FromDOMNode($oDomNode); + $aDashletOrder[] = array('rank' => $iRank, 'dashlet' => $oNewDashlet); + } + usort($aDashletOrder, array(get_class($this), 'SortOnRank')); + $aDashletList = array(); + foreach($aDashletOrder as $aItem) + { + $aDashletList[] = $aItem['dashlet']; + } + $aCellOrder[] = array('rank' => $iCellRank, 'dashlets' => $aDashletList); + } + } + usort($aCellOrder, array(get_class($this), 'SortOnRank')); + foreach($aCellOrder as $aItem) + { + $this->aCells[] = $aItem['dashlets']; + } + } + else + { + $this->aCells = array(); + } + } + static function SortOnRank($aItem1, $aItem2) { return ($aItem1['rank'] > $aItem2['rank']) ? +1 : -1; @@ -133,14 +157,24 @@ abstract class Dashboard $oMainNode->setAttribute('xmlns:xsi', "http://www.w3.org/2001/XMLSchema-instance"); $oDoc->appendChild($oMainNode); + $this->ToDOMNode($oMainNode); + + $sXml = $oDoc->saveXML(); + return $sXml; + } + + public function ToDOMNode($oDefinition) + { + $oDoc = $oDefinition->ownerDocument; + $oNode = $oDoc->createElement('layout', $this->sLayoutClass); - $oMainNode->appendChild($oNode); + $oDefinition->appendChild($oNode); $oNode = $oDoc->createElement('title', $this->sTitle); - $oMainNode->appendChild($oNode); + $oDefinition->appendChild($oNode); $oCellsNode = $oDoc->createElement('cells'); - $oMainNode->appendChild($oCellsNode); + $oDefinition->appendChild($oCellsNode); $iCellRank = 0; foreach ($this->aCells as $aCell) @@ -167,11 +201,9 @@ abstract class Dashboard $oDashlet->ToDOMNode($oNode); } } - - $sXml = $oDoc->saveXML(); - return $sXml; } + public function FromParams($aParams) { $this->sLayoutClass = $aParams['layout_class']; @@ -184,7 +216,7 @@ abstract class Dashboard { $sDashletClass = $aDashletParams['dashlet_class']; $sId = $aDashletParams['dashlet_id']; - $oNewDashlet = new $sDashletClass(new ModelReflectionRuntime(), $sId); + $oNewDashlet = new $sDashletClass($this->oMetaModel, $sId); $oForm = $oNewDashlet->GetForm(); $oForm->SetParamsContainer($sId); @@ -197,7 +229,7 @@ abstract class Dashboard } } - + public function Save() { @@ -267,24 +299,43 @@ abstract class Dashboard $oPage->add(''); $oForm = new DesignerForm(); + + $oField = new DesignerHiddenField('dashboard_id', '', $this->sId); + $oForm->AddField($oField); + $oField = new DesignerLongTextField('dashboard_title', Dict::S('UI:DashboardEdit:DashboardTitle'), $this->sTitle); $oForm->AddField($oField); $this->SetFormParams($oForm); - $oForm->RenderAsPropertySheet($oPage, false, ':itop-dashboard'); + $oForm->RenderAsPropertySheet($oPage, false, '.itop-dashboard'); $oPage->add(''); $oPage->add_ready_script( <<add(''); $oPage->add_ready_script("$('.dashlet_icon').draggable({helper: 'clone', appendTo: 'body', zIndex: 10000, revert:'invalid'});"); - $oPage->add_ready_script("$('.layout_cell').droppable({accept:'.dashlet_icon', hoverClass:'dragHover'});"); } public function RenderDashletsProperties($oPage) @@ -339,7 +389,7 @@ EOF $oPage->add(''); } } @@ -368,11 +418,12 @@ EOF class RuntimeDashboard extends Dashboard { protected $bCustomized; - + public function __construct($sId) { parent::__construct($sId); $this->bCustomized = false; + $this->oMetaModel = new ModelReflectionRuntime(); } public function SetCustomFlag($bCustomized) @@ -482,7 +533,28 @@ EOF ); } } - + + public function RenderProperties($oPage) + { + parent::RenderProperties($oPage); + + $oPage->add_ready_script( +<<add('
'); @@ -521,7 +593,7 @@ $('#dashboard_editor').dialog({ title: '$sDialogTitle', buttons: [ { text: "$sOkButtonLabel", click: function() { - var oDashboard = $(':itop-dashboard').data('itopDashboard'); + var oDashboard = $('.itop-dashboard').data('itopRuntimedashboard'); if (oDashboard.is_dirty()) { if (!confirm('$sAutoApplyConfirmationMessage')) @@ -537,7 +609,7 @@ $('#dashboard_editor').dialog({ oDashboard.save(); } }, { text: "$sCancelButtonLabel", click: function() { - var oDashboard = $(':itop-dashboard').data('itopDashboard'); + var oDashboard = $('.itop-dashboard').data('itopRuntimedashboard'); if (oDashboard.is_modified()) { if (!confirm('$sCancelConfirmationMessage')) @@ -553,40 +625,13 @@ $('#dashboard_editor').dialog({ close: function() { $(this).remove(); } }); -$('#dashboard_editor .ui-layout-center').dashboard({ +$('#dashboard_editor .ui-layout-center').runtimedashboard({ dashboard_id: '$sId', layout_class: '$sLayoutClass', title: '$sTitle', submit_to: '$sUrl', submit_parameters: {operation: 'save_dashboard'}, render_to: '$sUrl', render_parameters: {operation: 'render_dashboard'}, new_dashlet_parameters: {operation: 'new_dashlet'} }); -$('#select_dashlet').droppable({ - accept: '.dashlet', - drop: function(event, ui) { - $( this ).find( ".placeholder" ).remove(); - var oDashlet = ui.draggable; - oDashlet.remove(); - }, -}); - -$('#event_bus').bind('dashlet-selected', function(event, data){ - var sDashletId = data.dashlet_id; - var sPropId = 'dashlet_properties_'+sDashletId; - $('.dashlet_properties').each(function() { - var sId = $(this).attr('id'); - var bShow = (sId == sPropId); - if (bShow) - { - $(this).show(); - } - else - { - $(this).hide(); - } - }); - - }); - dashboard_prop_size = GetUserPreference('dashboard_prop_size', 350); $('#dashboard_editor').layout({ east: { @@ -607,7 +652,7 @@ $('#dashboard_editor').layout({ window.onbeforeunload = function() { if (!window.bLeavingOnUserAction) { - var oDashboard = $(':itop-dashboard').data('itopDashboard'); + var oDashboard = $('.itop-dashboard').data('itopRuntimedashboard'); if (oDashboard) { if (oDashboard.is_dirty()) @@ -688,7 +733,7 @@ EOF foreach($aDashlets as $sDashletClass => $aDashletInfo) { $oSubForm = new DesignerForm(); - $oDashlet = new $sDashletClass(new ModelReflectionRuntime(), 0); + $oDashlet = new $sDashletClass($this->oMetaModel, 0); $oDashlet->GetPropertiesFieldsFromOQL($oSubForm, $sOQL); $oSelectorField->AddSubForm($oSubForm, $aDashletInfo['label'], $aDashletInfo['class']); diff --git a/application/dashboardlayout.class.inc.php b/application/dashboardlayout.class.inc.php index 68bfd2a30..8132f77fa 100644 --- a/application/dashboardlayout.class.inc.php +++ b/application/dashboardlayout.class.inc.php @@ -107,14 +107,16 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout $oPage->add(''); $iCellIdx = 0; $fColSize = 100 / $this->iNbCols; - $sStyle = $bEditMode ? 'style="border: 1px #ccc dashed; width:'.$fColSize.'%;" class="layout_cell edit_mode"' : 'style="width: '.$fColSize.'%;" class="dashboard"'; + $sStyle = $bEditMode ? 'border: 1px #ccc dashed; width:'.$fColSize.'%;' : 'width: '.$fColSize.'%;'; + $sClass = $bEditMode ? 'layout_cell edit_mode' : 'dashboard'; $iNbRows = ceil(count($aCells) / $this->iNbCols); for($iRows = 0; $iRows < $iNbRows; $iRows++) { $oPage->add(''); for($iCols = 0; $iCols < $this->iNbCols; $iCols++) { - $oPage->add("'); for($iCols = 0; $iCols < $this->iNbCols; $iCols++) { diff --git a/application/dashlet.class.inc.php b/application/dashlet.class.inc.php index dc51c3a41..fa0f7f36c 100644 --- a/application/dashlet.class.inc.php +++ b/application/dashlet.class.inc.php @@ -87,11 +87,10 @@ abstract class Dashlet { foreach ($this->aProperties as $sProperty => $value) { - $this->oDOMNode = $oDOMNode->getElementsByTagName($sProperty)->item(0); - if ($this->oDOMNode != null) + $oPropNode = $oDOMNode->getElementsByTagName($sProperty)->item(0); + if ($oPropNode != null) { - $newvalue = $this->Str2Prop($sProperty, $this->oDOMNode->textContent); - $this->aProperties[$sProperty] = $newvalue; + $this->aProperties[$sProperty] = $this->PropertyFromDOMNode($oPropNode, $sProperty); } } } @@ -100,12 +99,26 @@ abstract class Dashlet { foreach ($this->aProperties as $sProperty => $value) { - $sXmlValue = $this->Prop2Str($value); - $oPropNode = $oDOMNode->ownerDocument->createElement($sProperty, $sXmlValue); + $oPropNode = $oDOMNode->ownerDocument->createElement($sProperty); $oDOMNode->appendChild($oPropNode); + $this->PropertyToDOMNode($oPropNode, $sProperty, $value); } } - + + + protected function PropertyFromDOMNode($oDOMNode, $sProperty) + { + $res = $this->Str2Prop($sProperty, $oDOMNode->textContent); + return $res; + } + + protected function PropertyToDOMNode($oDOMNode, $sProperty, $value) + { + $sXmlValue = $this->Prop2Str($value); + $oTextNode = $oDOMNode->ownerDocument->createTextNode($sXmlValue); + $oDOMNode->appendChild($oTextNode); + } + public function FromXml($sXml) { $oDomDoc = new DOMDocument('1.0', 'UTF-8'); @@ -139,10 +152,24 @@ abstract class Dashlet $oPage->add('
'); } } + else + { + foreach ($this->aCSSClasses as $sCSSClass) + { + $oPage->add_ready_script("$('#dashlet_".$sId."').addClass('$sCSSClass');"); + } + } try { - $this->Render($oPage, $bEditMode, $aExtraParams); + if (get_class($this->oModelReflection) == 'ModelReflectionRuntime') + { + $this->Render($oPage, $bEditMode, $aExtraParams); + } + else + { + $this->RenderNoData($oPage, $bEditMode, $aExtraParams); + } } catch(UnknownClassOqlException $e) { @@ -195,6 +222,12 @@ EOF } abstract public function Render($oPage, $bEditMode = false, $aExtraParams = array()); + + /* Rendering without the real data */ + public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = array()) + { + $this->Render($oPage, $bEditMode, $aExtraParams); + } abstract public function GetPropertiesFields(DesignerForm $oForm); @@ -312,7 +345,7 @@ class DashletPlainText extends Dashlet $sText = htmlentities($this->aProperties['text'], ENT_QUOTES, 'UTF-8'); $sId = 'plaintext_'.($bEditMode? 'edit_' : '').$this->sId; - $oPage->add('
'.$sText.'
'); + $oPage->add('
'.$sText.'
'); } public function GetPropertiesFields(DesignerForm $oForm) @@ -365,6 +398,27 @@ class DashletObjectList extends Dashlet $oPage->add('
'); } + public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = array()) + { + $sTitle = $this->aProperties['title']; + $sQuery = $this->aProperties['query']; + $sShowMenu = $this->aProperties['menu'] ? '1' : '0'; + + $oPage->add('
'); + $sHtmlTitle = htmlentities($this->oModelReflection->DictString($sTitle), ENT_QUOTES, 'UTF-8'); // done in the itop block + if ($sHtmlTitle != '') + { + $oPage->add('

'.$sHtmlTitle.'

'); + } + $oQuery = $this->oModelReflection->GetQuery($sQuery); + $sClass = $oQuery->GetClass(); + $oPage->add('
'); + $oPage->p(Dict::S('UI:NoObjectToDisplay')); + $oPage->p(''.Dict::Format('UI:ClickToCreateNew', $this->oModelReflection->GetName($sClass)).''); + $oPage->add('
'); + $oPage->add('
'); + } + public function GetPropertiesFields(DesignerForm $oForm) { $oField = new DesignerTextField('title', Dict::S('UI:DashletObjectList:Prop-Title'), $this->aProperties['title']); @@ -531,10 +585,74 @@ abstract class DashletGroupBy extends Dashlet } } + public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = array()) + { + $sTitle = $this->aProperties['title']; + $sQuery = $this->aProperties['query']; + $sGroupBy = $this->aProperties['group_by']; + $sStyle = $this->aProperties['style']; + + $oQuery = $this->oModelReflection->GetQuery($sQuery); + $sClass = $oQuery->GetClass(); + + + $aDisplayValues = array(); + $sAttLabel = ''; + if ($this->oModelReflection->IsValidAttCode($sClass, $sGroupBy)) + { + $sAttLabel = $this->oModelReflection->GetLabel($sClass, $sGroupBy); + $aAllowed = $this->oModelReflection->GetAllowedValues_att($sClass, $sGroupBy); + if ($aAllowed) // null for non enums + { + $iTotal = 0; + foreach ($aAllowed as $sValue => $sValueLabel) + { + $iCount = (int) rand(2, 100); + $iTotal += $iCount; + $aDisplayValues[] = array( + 'label' => $sValueLabel, + 'count' => $iCount + ); + } + } + } + else + { + $oPage->add('
Sorry, grouping by date/time cannot be viewed in the designer
'); + } + + $oPage->add('
'); + + $sBlockId = 'block_fake_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occuring in the same DOM) + + $oPage->add('
'); + $oPage->add('

'.Dict::Format('UI:Pagination:HeaderNoSelection', $iTotal).'

'); + $oPage->add('
"); + $sCellClass = ($iRows == $iNbRows-1) ? $sClass.' layout_last_used_rank' : $sClass; + $oPage->add(""); if (array_key_exists($iCellIdx, $aCells)) { $aDashlets = $aCells[$iCellIdx]; @@ -144,7 +146,7 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout } if ($bEditMode) // Add one row for extensibility { - $sStyle = 'style="border: 1px #ccc dashed; width:'.$fColSize.'%;" class="layout_cell edit_mode layout_extension"'; + $sStyle = 'style="border: 1px #ccc dashed; width:'.$fColSize.'%;" class="layout_cell edit_mode layout_extension" data-dashboard-cell-index="'.$iCellIdx.'"'; $oPage->add('
'); + $oPage->add(''); + $oPage->add(''); + $oPage->add(''); + $oPage->add(''); + $oPage->add(''); + $oPage->add(''); + $oPage->add(''); + foreach($aDisplayValues as $aDisplayData) + { + $oPage->add(''); + $oPage->add(''); + $oPage->add(''); + $oPage->add(''); + } + $oPage->add(''); + $oPage->add('
'.$sAttLabel.''.Dict::S('UI:GroupBy:Count').'
'.$aDisplayData['label'].''.$aDisplayData['count'].'
'); + $oPage->add('
'); + + $oPage->add(''); + } + protected function GetGroupByOptions($sOql) { - $oSearch = DBObjectSearch::FromOQL($sOql); - $sClass = $oSearch->GetClass(); + $oQuery = $this->oModelReflection->GetQuery($sOql); + $sClass = $oQuery->GetClass(); $aGroupBy = array(); foreach($this->oModelReflection->ListAttributes($sClass) as $sAttCode => $sAttType) { @@ -604,11 +722,11 @@ abstract class DashletGroupBy extends Dashlet try { $sCurrQuery = $aValues['query']; - $oCurrSearch = DBObjectSearch::FromOQL($sCurrQuery); + $oCurrSearch = $this->oModelReflection->GetQuery($sCurrQuery); $sCurrClass = $oCurrSearch->GetClass(); $sPrevQuery = $this->aProperties['query']; - $oPrevSearch = DBObjectSearch::FromOQL($sPrevQuery); + $oPrevSearch = $this->oModelReflection->GetQuery($sPrevQuery); $sPrevClass = $oPrevSearch->GetClass(); if ($sCurrClass != $sPrevClass) @@ -631,15 +749,15 @@ abstract class DashletGroupBy extends Dashlet { // Style changed, mutate to the specified type of chart case 'pie': - $oDashlet = new DashletGroupByPie($this->sId); + $oDashlet = new DashletGroupByPie($this->oModelReflection, $this->sId); break; case 'bars': - $oDashlet = new DashletGroupByBars($this->sId); + $oDashlet = new DashletGroupByBars($this->oModelReflection, $this->sId); break; case 'table': - $oDashlet = new DashletGroupByTable($this->sId); + $oDashlet = new DashletGroupByTable($this->oModelReflection, $this->sId); break; } $oDashlet->FromParams($aValues); @@ -756,9 +874,8 @@ class DashletHeaderStatic extends Dashlet { parent::__construct($oModelReflection, $sId); $this->aProperties['title'] = Dict::S('UI:DashletHeaderStatic:Prop-Title:Default'); - $sIcon = $this->oModelReflection->GetClassIcon('Contact', false); - $sIcon = str_replace(utils::GetAbsoluteUrlModulesRoot(), '', $sIcon); - $this->aProperties['icon'] = $sIcon; + $oIconSelect = $this->oModelReflection->GetIconSelectionField('icon'); + $this->aProperties['icon'] = $oIconSelect->GetDefaultValue('Contact'); } public function Render($oPage, $bEditMode = false, $aExtraParams = array()) @@ -766,13 +883,14 @@ class DashletHeaderStatic extends Dashlet $sTitle = $this->aProperties['title']; $sIcon = $this->aProperties['icon']; - $sIconPath = utils::GetAbsoluteUrlModulesRoot().$sIcon; + $oIconSelect = $this->oModelReflection->GetIconSelectionField('icon'); + $sIconPath = $oIconSelect->MakeFileUrl($sIcon); $oPage->add('
'); $oPage->add('
'); $oPage->add(''); - $oPage->add('

'.Dict::S($sTitle).'

'); + $oPage->add('

'.$this->oModelReflection->DictString($sTitle).'

'); $oPage->add('
'); $oPage->add('
'); @@ -783,18 +901,36 @@ class DashletHeaderStatic extends Dashlet $oField = new DesignerTextField('title', Dict::S('UI:DashletHeaderStatic:Prop-Title'), $this->aProperties['title']); $oForm->AddField($oField); - $oField = new DesignerIconSelectionField('icon', Dict::S('UI:DashletHeaderStatic:Prop-Icon'), $this->aProperties['icon']); - $aAllIcons = self::FindIcons(APPROOT.'env-'.utils::GetCurrentEnvironment()); - ksort($aAllIcons); - $aValues = array(); - foreach($aAllIcons as $sFilePath) - { - $aValues[] = array('value' => $sFilePath, 'label' => basename($sFilePath), 'icon' => utils::GetAbsoluteUrlModulesRoot().$sFilePath); - } - $oField->SetAllowedValues($aValues); + $oField = $this->oModelReflection->GetIconSelectionField('icon', Dict::S('UI:DashletHeaderStatic:Prop-Icon'), $this->aProperties['icon']); $oForm->AddField($oField); } + protected function PropertyFromDOMNode($oDOMNode, $sProperty) + { + if ($sProperty == 'icon') + { + $oIconField = $this->oModelReflection->GetIconSelectionField('icon'); + return $oIconField->ValueFromDOMNode($oDOMNode); + } + else + { + return parent::PropertyFromDOMNode($oDOMNode, $sProperty); + } + } + + protected function PropertyToDOMNode($oDOMNode, $sProperty, $value) + { + if ($sProperty == 'icon') + { + $oIconField = $this->oModelReflection->GetIconSelectionField('icon'); + $oIconField->ValueToDOMNode($oDOMNode, $value); + } + else + { + parent::PropertyToDOMNode($oDOMNode, $sProperty, $value); + } + } + static public function GetInfo() { return array( @@ -803,30 +939,6 @@ class DashletHeaderStatic extends Dashlet 'description' => Dict::S('UI:DashletHeaderStatic:Description'), ); } - - static public function FindIcons($sBaseDir, $sDir = '') - { - $aResult = array(); - // Populate automatically the list of icon files - if ($hDir = @opendir($sBaseDir.'/'.$sDir)) - { - while (($sFile = readdir($hDir)) !== false) - { - $aMatches = array(); - if (($sFile != '.') && ($sFile != '..') && ($sFile != 'lifecycle') && is_dir($sBaseDir.'/'.$sDir.'/'.$sFile)) - { - $sDirSubPath = ($sDir == '') ? $sFile : $sDir.'/'.$sFile; - $aResult = array_merge($aResult, self::FindIcons($sBaseDir, $sDirSubPath)); - } - if (preg_match("/\.(png|jpg|jpeg|gif)$/i", $sFile, $aMatches)) // png, jp(e)g and gif are considered valid - { - $aResult[$sFile.'_'.$sDir] = $sDir.'/'.$sFile; - } - } - closedir($hDir); - } - return $aResult; - } } @@ -836,28 +948,27 @@ class DashletHeaderDynamic extends Dashlet { parent::__construct($oModelReflection, $sId); $this->aProperties['title'] = Dict::S('UI:DashletHeaderDynamic:Prop-Title:Default'); - $sIcon = $this->oModelReflection->GetClassIcon('Contact', false); - $sIcon = str_replace(utils::GetAbsoluteUrlModulesRoot(), '', $sIcon); - $this->aProperties['icon'] = $sIcon; + $oIconSelect = $this->oModelReflection->GetIconSelectionField('icon'); + $this->aProperties['icon'] = $oIconSelect->GetDefaultValue('Contact'); $this->aProperties['subtitle'] = Dict::S('UI:DashletHeaderDynamic:Prop-Subtitle:Default'); $this->aProperties['query'] = 'SELECT Contact'; $this->aProperties['group_by'] = 'status'; $this->aProperties['values'] = array('active', 'inactive'); } - - public function Render($oPage, $bEditMode = false, $aExtraParams = array()) + + protected function GetValues() { - $sTitle = $this->aProperties['title']; - $sIcon = $this->aProperties['icon']; - $sSubtitle = $this->aProperties['subtitle']; $sQuery = $this->aProperties['query']; $sGroupBy = $this->aProperties['group_by']; $aValues = $this->aProperties['values']; - $oFilter = DBObjectSearch::FromOQL($sQuery); - $sClass = $oFilter->GetClass(); + if (empty($aValues)) + { + $aValues = array(); + } - $sIconPath = utils::GetAbsoluteUrlModulesRoot().$sIcon; + $oQuery = $this->oModelReflection->GetQuery($sQuery); + $sClass = $oQuery->GetClass(); if ($this->oModelReflection->IsValidAttCode($sClass, $sGroupBy)) { @@ -870,6 +981,21 @@ class DashletHeaderDynamic extends Dashlet } } } + return $aValues; + } + + public function Render($oPage, $bEditMode = false, $aExtraParams = array()) + { + $sTitle = $this->aProperties['title']; + $sIcon = $this->aProperties['icon']; + $sSubtitle = $this->aProperties['subtitle']; + $sQuery = $this->aProperties['query']; + $sGroupBy = $this->aProperties['group_by']; + + $oIconSelect = $this->oModelReflection->GetIconSelectionField('icon'); + $sIconPath = $oIconSelect->MakeFileUrl($sIcon); + + $aValues = $this->GetValues(); if (count($aValues) > 0) { // Stats grouped by @@ -897,6 +1023,7 @@ class DashletHeaderDynamic extends Dashlet $oPage->add(''); + $oFilter = DBObjectSearch::FromOQL($sQuery); $oBlock = new DisplayBlock($oFilter, 'summary'); $sBlockId = 'block_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occuring in the same DOM) $oBlock->Display($oPage, $sBlockId, $aExtraParams); @@ -905,20 +1032,78 @@ class DashletHeaderDynamic extends Dashlet $oPage->add(''); } + public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = array()) + { + $sTitle = $this->aProperties['title']; + $sIcon = $this->aProperties['icon']; + $sSubtitle = $this->aProperties['subtitle']; + $sQuery = $this->aProperties['query']; + $sGroupBy = $this->aProperties['group_by']; + $aValues = $this->aProperties['values']; + + $oQuery = $this->oModelReflection->GetQuery($sQuery); + $sClass = $oQuery->GetClass(); + + $oIconSelect = $this->oModelReflection->GetIconSelectionField('icon'); + $sIconPath = $oIconSelect->MakeFileUrl($sIcon); + + $oPage->add('
'); + $oPage->add('
'); + + $oPage->add(''); + + $sBlockId = 'block_fake_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occuring in the same DOM) + + $iTotal = 0; + $aValues = $this->GetValues(); + if (count($aValues) > 0) + { + // Stats grouped by + } + else + { + // Simple stats + } + + $oPage->add('
'); + $oPage->add('
'); + $oPage->add(''); + $oPage->add(''); + foreach ($aValues as $sValue) + { + $sValueLabel = $this->oModelReflection->GetValueLabel($sClass, $sGroupBy, $sValue); + $oPage->add(' '); + } + $oPage->add(''); + $oPage->add(''); + foreach ($aValues as $sValue) + { + $iCount = (int) rand(2, 100); + $iTotal += $iCount; + $oPage->add(' '); + } + $oPage->add(''); + $oPage->add('
'.$sValueLabel.'
'.$iCount.'
'); + $oPage->add('
'); + + $sTitle = $this->oModelReflection->DictString($sTitle); + $sSubtitle = $this->oModelReflection->DictFormat($sSubtitle, $iTotal); +// $sSubtitle = "original: $sSubtitle, S:".$this->oModelReflection->DictString($sSubtitle).", Format: '".$this->oModelReflection->DictFormat($sSubtitle, $iTotal)."'"; + + $oPage->add('

'.$sTitle.'

'); + $oPage->add(''.$sSubtitle.''); + $oPage->add('
'); + + $oPage->add('
'); + $oPage->add('
'); + } + public function GetPropertiesFields(DesignerForm $oForm) { $oField = new DesignerTextField('title', Dict::S('UI:DashletHeaderDynamic:Prop-Title'), $this->aProperties['title']); $oForm->AddField($oField); - $oField = new DesignerIconSelectionField('icon', Dict::S('UI:DashletHeaderDynamic:Prop-Icon'), $this->aProperties['icon']); - $aAllIcons = DashletHeaderStatic::FindIcons(APPROOT.'env-'.utils::GetCurrentEnvironment()); - ksort($aAllIcons); - $aValues = array(); - foreach($aAllIcons as $sFilePath) - { - $aValues[] = array('value' => $sFilePath, 'label' => basename($sFilePath), 'icon' => utils::GetAbsoluteUrlModulesRoot().$sFilePath); - } - $oField->SetAllowedValues($aValues); + $oField = $this->oModelReflection->GetIconSelectionField('icon', Dict::S('UI:DashletHeaderDynamic:Prop-Icon'), $this->aProperties['icon']); $oForm->AddField($oField); $oField = new DesignerTextField('subtitle', Dict::S('UI:DashletHeaderDynamic:Prop-Subtitle'), $this->aProperties['subtitle']); @@ -931,8 +1116,8 @@ class DashletHeaderDynamic extends Dashlet try { // Group by field: build the list of possible values (attribute codes + ...) - $oSearch = DBObjectSearch::FromOQL($this->aProperties['query']); - $sClass = $oSearch->GetClass(); + $oQuery = $this->oModelReflection->GetQuery($this->aProperties['query']); + $sClass = $oQuery->GetClass(); $aGroupBy = array(); foreach($this->oModelReflection->ListAttributes($sClass, 'AttributeEnum,AttributeFinalClass') as $sAttCode => $sAttType) { @@ -975,11 +1160,11 @@ class DashletHeaderDynamic extends Dashlet try { $sCurrQuery = $aValues['query']; - $oCurrSearch = DBObjectSearch::FromOQL($sCurrQuery); + $oCurrSearch = $this->oModelReflection->GetQuery($sCurrQuery); $sCurrClass = $oCurrSearch->GetClass(); $sPrevQuery = $this->aProperties['query']; - $oPrevSearch = DBObjectSearch::FromOQL($sPrevQuery); + $oPrevSearch = $this->oModelReflection->GetQuery($sPrevQuery); $sPrevClass = $oPrevSearch->GetClass(); if ($sCurrClass != $sPrevClass) @@ -1003,6 +1188,32 @@ class DashletHeaderDynamic extends Dashlet return parent::Update($aValues, $aUpdatedFields); } + protected function PropertyFromDOMNode($oDOMNode, $sProperty) + { + if ($sProperty == 'icon') + { + $oIconField = $this->oModelReflection->GetIconSelectionField('icon'); + return $oIconField->ValueFromDOMNode($oDOMNode); + } + else + { + return parent::PropertyFromDOMNode($oDOMNode, $sProperty); + } + } + + protected function PropertyToDOMNode($oDOMNode, $sProperty, $value) + { + if ($sProperty == 'icon') + { + $oIconField = $this->oModelReflection->GetIconSelectionField('icon'); + $oIconField->ValueToDOMNode($oDOMNode, $value); + } + else + { + parent::PropertyToDOMNode($oDOMNode, $sProperty, $value); + } + } + static public function GetInfo() { return array( @@ -1023,7 +1234,7 @@ class DashletBadge extends Dashlet $this->aCSSClasses[] = 'dashlet-inline'; $this->aCSSClasses[] = 'dashlet-badge'; } - + public function Render($oPage, $bEditMode = false, $aExtraParams = array()) { $sClass = $this->aProperties['class']; @@ -1039,52 +1250,63 @@ class DashletBadge extends Dashlet $oBlock->Display($oPage, $sBlockId, $aExtraParams); $oPage->add(''); - if ($bEditMode) - { - // Since the container div is not rendered the same way in edit mode, add the 'inline' style to it - $oPage->add_ready_script("$('#dashlet_".$this->sId."').addClass('dashlet-inline');"); - } } + public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = array()) + { + $sClass = $this->aProperties['class']; + + $sIconUrl = $this->oModelReflection->GetClassIcon($sClass, false); + $sClassLabel = $this->oModelReflection->GetName($sClass); + + $oPage->add('
'); + + $oPage->add('
'); + $oPage->add('

'); + $oPage->add(' '.$sClassLabel.': 947'); + $oPage->add('

'); + $oPage->add('

'); + $oPage->add(' '.Dict::Format('UI:ClickToCreateNew', $sClassLabel).''); + $oPage->add('
'); + $oPage->add(' Search for Server objects'); + $oPage->add('

'); + $oPage->add('
'); + + $oPage->add('
'); + } + + static protected $aClassList = null; + public function GetPropertiesFields(DesignerForm $oForm) { - - $oClassesSet = new ValueSetEnumClasses('bizmodel', array()); - $aClasses = $oClassesSet->GetValues(array()); - - $aLinkClasses = array(); - - foreach($this->oModelReflection->GetClasses('bizmodel') as $sClass) - { - foreach($this->oModelReflection->ListAttributes($sClass, 'AttributeLinkedSetIndirect') as $sAttCode => $sAttType) - { - $sLinkedClass = $this->oModelReflection->GetAttributeProperty($sClass, $sAttCode, 'linked_class'); - if ($sLinkedClass != null) - { - $aLinkClasses[$sLinkedClass] = true; - } - } - } - - - $oField = new DesignerIconSelectionField('class', Dict::S('UI:DashletBadge:Prop-Class'), $this->aProperties['class']); - ksort($aClasses); - $aValues = array(); - foreach($aClasses as $sClass => $sClass) + if (is_null(self::$aClassList)) { - if (!array_key_exists($sClass, $aLinkClasses)) + // Cache the ordered list of classes (ordered on the label) + // (has a significant impact when editing a page with lots of badges) + // + $aClasses = array(); + foreach($this->oModelReflection->GetClasses('bizmodel', true /*exclude links*/) as $sClass) + { + $aClasses[$sClass] = $this->oModelReflection->GetName($sClass); + } + asort($aClasses); + + self::$aClassList = array(); + foreach($aClasses as $sClass => $sLabel) { $sIconUrl = $this->oModelReflection->GetClassIcon($sClass, false); $sIconFilePath = str_replace(utils::GetAbsoluteUrlAppRoot(), APPROOT, $sIconUrl); - if (($sIconUrl == '') || !file_exists($sIconFilePath)) + if ($sIconUrl == '') { - // The icon does not exist, leet's use a transparent one of the same size. + // The icon does not exist, let's use a transparent one of the same size. $sIconUrl = utils::GetAbsoluteUrlAppRoot().'images/transparent_32_32.png'; } - $aValues[] = array('value' => $sClass, 'label' => $this->oModelReflection->GetName($sClass), 'icon' => $sIconUrl); + self::$aClassList[] = array('value' => $sClass, 'label' => $sLabel, 'icon' => $sIconUrl); } } - $oField->SetAllowedValues($aValues); + + $oField = new DesignerIconSelectionField('class', Dict::S('UI:DashletBadge:Prop-Class'), $this->aProperties['class']); + $oField->SetAllowedValues(self::$aClassList); $oForm->AddField($oField); } diff --git a/application/forms.class.inc.php b/application/forms.class.inc.php index f42fd7836..79e98da2e 100644 --- a/application/forms.class.inc.php +++ b/application/forms.class.inc.php @@ -828,19 +828,26 @@ class DesignerHiddenField extends DesignerFormField class DesignerIconSelectionField extends DesignerFormField { + protected $sUploadUrl; protected $aAllowedValues; public function __construct($sCode, $sLabel = '', $defaultValue = '') { parent::__construct($sCode, $sLabel, $defaultValue); $this->bAutoApply = true; + $this->sUploadUrl = null; } public function SetAllowedValues($aAllowedValues) { $this->aAllowedValues = $aAllowedValues; } - + + public function EnableUpload($sIconUploadUrl) + { + $this->sUploadUrl = $sIconUploadUrl; + } + public function Render(WebPage $oP, $sFormId, $sRenderMode='dialog') { $sId = $this->oForm->GetFieldId($this->sCode); @@ -868,6 +875,71 @@ EOF } } +class RunTimeIconSelectionField extends DesignerIconSelectionField +{ + public function __construct($sCode, $sLabel = '', $defaultValue = '') + { + parent::__construct($sCode, $sLabel, $defaultValue); + + $aAllIcons = self::FindIconsOnDisk(APPROOT.'env-'.utils::GetCurrentEnvironment()); + ksort($aAllIcons); + $aValues = array(); + foreach($aAllIcons as $sFilePath) + { + $aValues[] = array('value' => $sFilePath, 'label' => basename($sFilePath), 'icon' => utils::GetAbsoluteUrlModulesRoot().$sFilePath); + } + $this->SetAllowedValues($aValues); + } + + static protected function FindIconsOnDisk($sBaseDir, $sDir = '') + { + $aResult = array(); + // Populate automatically the list of icon files + if ($hDir = @opendir($sBaseDir.'/'.$sDir)) + { + while (($sFile = readdir($hDir)) !== false) + { + $aMatches = array(); + if (($sFile != '.') && ($sFile != '..') && ($sFile != 'lifecycle') && is_dir($sBaseDir.'/'.$sDir.'/'.$sFile)) + { + $sDirSubPath = ($sDir == '') ? $sFile : $sDir.'/'.$sFile; + $aResult = array_merge($aResult, self::FindIconsOnDisk($sBaseDir, $sDirSubPath)); + } + if (preg_match("/\.(png|jpg|jpeg|gif)$/i", $sFile, $aMatches)) // png, jp(e)g and gif are considered valid + { + $aResult[$sFile.'_'.$sDir] = $sDir.'/'.$sFile; + } + } + closedir($hDir); + } + return $aResult; + } + + public function ValueFromDOMNode($oDOMNode) + { + return $oDOMNode->textContent; + } + + public function ValueToDOMNode($oDOMNode, $value) + { + $oTextNode = $oDOMNode->ownerDocument->createTextNode($value); + $oDOMNode->appendChild($oTextNode); + } + + public function MakeFileUrl($value) + { + return utils::GetAbsoluteUrlModulesRoot().$value; + } + + public function GetDefaultValue($sClass = 'Contact') + { + $sIconPath = MetaModel::GetClassIcon($sClass, false); + $sIcon = str_replace(utils::GetAbsoluteUrlModulesRoot(), '', $sIconPath); + return $sIcon; + } +} + + class DesignerSortableField extends DesignerFormField { protected $aAllowedValues; diff --git a/core/modelreflection.class.inc.php b/core/modelreflection.class.inc.php index c530e2d32..338460957 100644 --- a/core/modelreflection.class.inc.php +++ b/core/modelreflection.class.inc.php @@ -24,25 +24,59 @@ * @license http://opensource.org/licenses/AGPL-3.0 */ -interface ModelReflection +abstract class ModelReflection { - public function GetClassIcon($sClass, $bImgTag = true); - public function IsValidAttCode($sClass, $sAttCode); - public function GetName($sClass); - public function GetLabel($sClass, $sAttCodeEx); - public function ListAttributes($sClass, $sScope = null); - public function GetAttributeProperty($sClass, $sAttCode, $sPropName, $default = null); - public function GetAllowedValues_att($sClass, $sAttCode); - public function HasChildrenClasses($sClass); - public function GetClasses($sCategories = ''); - public function IsValidClass($sClass); - public function IsSameFamilyBranch($sClassA, $sClassB); - public function GetParentClass($sClass); - public function GetFiltersList($sClass); - public function IsValidFilterCode($sClass, $sFilterCode); + abstract public function GetClassIcon($sClass, $bImgTag = true); + abstract public function IsValidAttCode($sClass, $sAttCode); + abstract public function GetName($sClass); + abstract public function GetLabel($sClass, $sAttCodeEx); + abstract public function GetValueLabel($sClass, $sAttCode, $sValue); + abstract public function ListAttributes($sClass, $sScope = null); + abstract public function GetAttributeProperty($sClass, $sAttCode, $sPropName, $default = null); + abstract public function GetAllowedValues_att($sClass, $sAttCode); + abstract public function HasChildrenClasses($sClass); + abstract public function GetClasses($sCategories = '', $bExcludeLinks = false); + abstract public function IsValidClass($sClass); + abstract public function IsSameFamilyBranch($sClassA, $sClassB); + abstract public function GetParentClass($sClass); + abstract public function GetFiltersList($sClass); + abstract public function IsValidFilterCode($sClass, $sFilterCode); + + abstract public function GetQuery($sOQL); + + abstract public function DictString($sStringCode, $sDefault = null, $bUserLanguageOnly = false); + + public function DictFormat($sFormatCode /*, ... arguments ....*/) + { + $sLocalizedFormat = $this->DictString($sFormatCode); + $aArguments = func_get_args(); + array_shift($aArguments); + + if ($sLocalizedFormat == $sFormatCode) + { + // Make sure the information will be displayed (ex: an error occuring before the dictionary gets loaded) + return $sFormatCode.' - '.implode(', ', $aArguments); + } + + return vsprintf($sLocalizedFormat, $aArguments); + } + + abstract public function GetIconSelectionField($sCode, $sLabel = '', $defaultValue = ''); } -class ModelReflectionRuntime implements ModelReflection +abstract class QueryReflection +{ + /** + * Throws an exception in case of an invalid syntax + */ + abstract public function __construct($sOQL); + + abstract public function GetClass(); + abstract public function GetClassAlias(); +} + + +class ModelReflectionRuntime extends ModelReflection { public function __construct() { @@ -68,6 +102,12 @@ class ModelReflectionRuntime implements ModelReflection return MetaModel::GetLabel($sClass, $sAttCodeEx); } + public function GetValueLabel($sClass, $sAttCode, $sValue) + { + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + return $oAttDef->GetValueLabel($sValue); + } + public function ListAttributes($sClass, $sScope = null) { $aScope = null; @@ -133,9 +173,26 @@ class ModelReflectionRuntime implements ModelReflection return MetaModel::HasChildrenClasses($sClass); } - public function GetClasses($sCategories = '') + public function GetClasses($sCategories = '', $bExcludeLinks = false) { - return MetaModel::GetClasses($sCategories); + $aClasses = MetaModel::GetClasses($sCategories); + if ($bExcludeLinks) + { + $aExcluded = ProfilesConfig::GetLinkClasses(); // table computed at compile time + $aRes = array(); + foreach ($aClasses as $sClass) + { + if (!array_key_exists($sClass, $aExcluded)) + { + $aRes[] = $sClass; + } + } + } + else + { + $aRes = $aClasses; + } + return $aRes; } public function IsValidClass($sClass) @@ -162,4 +219,43 @@ class ModelReflectionRuntime implements ModelReflection { return MetaModel::IsValidFilterCode($sClass, $sFilterCode); } -} \ No newline at end of file + + public function GetQuery($sOQL) + { + return new QueryReflectionRuntime($sOQL); + } + + public function DictString($sStringCode, $sDefault = null, $bUserLanguageOnly = false) + { + return Dict::S($sStringCode, $sDefault, $bUserLanguageOnly); + } + + public function GetIconSelectionField($sCode, $sLabel = '', $defaultValue = '') + { + return new RunTimeIconSelectionField($sCode, $sLabel, $defaultValue); + } +} + + +class QueryReflectionRuntime extends QueryReflection +{ + protected $oFilter; + + /** + * throws an exception in case of a wrong syntax + */ + public function __construct($sOQL) + { + $this->oFilter = DBObjectSearch::FromOQL($sOQL); + } + + public function GetClass() + { + return $this->oFilter->GetClass(); + } + + public function GetClassAlias() + { + return $this->oFilter->GetClassAlias(); + } +} diff --git a/js/dashboard.js b/js/dashboard.js index d2abfd68f..9c5862b52 100644 --- a/js/dashboard.js +++ b/js/dashboard.js @@ -1,4 +1,9 @@ // jQuery UI style "widget" for editing an iTop "dashboard" + +//////////////////////////////////////////////////////////////////////////////// +// +// dashboard +// $(function() { // the widget definition, where "itop" is the namespace, @@ -25,23 +30,18 @@ $(function() this.element .addClass('itop-dashboard') - .bind('mark_as_modified.itop-dashboard', function(){me.mark_as_modified();} ); + .bind('add_dashlet.itop_dashboard', function(event, oParams){ + me.add_dashlet(oParams); + }); - this.ajax_div = $('
').appendTo(this.element); + this.ajax_div = $('
'); + this.element.after(this.ajax_div); this._make_draggable(); - this.bModified = false; - }, // called when created, and later when changing options _refresh: function() { - var oParams = this._get_state(this.options.render_parameters); - var me = this; - $.post(this.options.render_to, oParams, function(data){ - me.element.html(data); - me._make_draggable(); - }); }, // events bound via _bind are removed automatically // revert other modifications here @@ -58,7 +58,6 @@ $(function() { // in 1.9 would use _superApply this._superApply(arguments); - this._refresh(); }, // _setOption is called for each individual option that is changing _setOption: function( key, value ) @@ -96,6 +95,170 @@ $(function() return oState; }, + _get_new_id: function() + { + var iMaxId = 0; + this.element.find(':itop-dashlet').each(function() { + var oDashlet = $(this).data('itopDashlet'); + if(oDashlet) + { + var oDashletParams = oDashlet.get_params(); + var id = parseInt(oDashletParams.dashlet_id, 10); + if (id > iMaxId) iMaxId = id; + } + }); + return 1 + iMaxId; + }, + _make_draggable: function() + { + var me = this; + this.element.find('.dashlet').draggable({ + revert: 'invalid', appendTo: 'body', zIndex: 9999, + helper: function() { + var oDragItem = $(this).dashlet('get_drag_icon'); + return oDragItem; + }, + cursorAt: { top: 16, left: 16 } + }); + this.element.find('table td').droppable({ + accept: '.dashlet,.dashlet_icon', + drop: function(event, ui) { + $( this ).find( ".placeholder" ).remove(); + var bRefresh = $(this).hasClass('layout_extension'); + var oDropped = ui.draggable; + if (oDropped.hasClass('dashlet')) + { + // moving around a dashlet + oDropped.detach(); + oDropped.css({top: 0, left: 0}); + oDropped.appendTo($(this)); + + var oDashlet = ui.draggable.data('itopDashlet'); + me.on_dashlet_moved(oDashlet, $(this), bRefresh); + } + else + { + // inserting a new dashlet + var sDashletClass = ui.draggable.attr('dashlet_class'); + $('.itop-dashboard').trigger('add_dashlet', {dashlet_class: sDashletClass, container: $(this), refresh: bRefresh }); + } + } + }); + }, + add_dashlet: function(options) + { + // 1) Create empty divs for the dashlet and its properties + // + var sDashletId = this._get_new_id(); + var oDashlet = $('
'); + oDashlet.appendTo(options.container); + var oDashletProperties = $('
'); + oDashletProperties.appendTo($('#dashlet_properties')); + + // 2) Ajax call to fill the divs with default values + // => in return, it must call add_dashlet_finalize + // + this.add_dashlet_ajax(options, sDashletId); + }, + add_dashlet_finalize: function(options, sDashletId, sDashletClass) + { + $('#dashlet_'+sDashletId) + .dashlet({dashlet_id: sDashletId, dashlet_class: sDashletClass}) + .dashlet('deselect_all') + .dashlet('select') + .draggable({ + revert: 'invalid', appendTo: 'body', zIndex: 9999, + helper: function() { + var oDragItem = $(this).dashlet('get_drag_icon'); + return oDragItem; + }, + cursorAt: { top: 16, left: 16 } + }); + if (options.refresh) + { + this._refresh(); + } + }, + on_dashlet_moved: function(oDashlet, oReceiver, bRefresh) + { + if (bRefresh) + { + // The layout was extended... refresh the whole dashboard + this._refresh(); + } + } + }); +}); + +//////////////////////////////////////////////////////////////////////////////// +// +// runtimedashboard (extends dashboard) +// +$(function() +{ + // the widget definition, where "itop" is the namespace, + // "dashboard" the widget name + $.widget( "itop.runtimedashboard", $.itop.dashboard, + { + // default options + options: + { + dashboard_id: '', + layout_class: '', + title: '', + submit_to: 'index.php', + submit_parameters: {}, + render_to: 'index.php', + render_parameters: {}, + new_dashlet_parameters: {} + }, + + // the constructor + _create: function() + { + var me = this; + + this._superApply(arguments); + + this.element + .addClass('itop-runtimedashboard') + .bind('mark_as_modified.itop-dashboard', function(){me.mark_as_modified();} ); + + this.bModified = false; + }, + + // events bound via _bind are removed automatically + // revert other modifications here + _destroy: function() + { + this.element + .removeClass('itop-runtimedashboard'); + + this._superApply(arguments); + }, + // _setOptions is called with a hash of all options that are changing + _setOptions: function() + { + this._superApply(arguments); + this._refresh(); + }, + // _setOption is called for each individual option that is changing + _setOption: function( key, value ) + { + this._superApply(arguments); + }, + // called when created, and later when changing options + _refresh: function() + { + this._super(); + + var oParams = this._get_state(this.options.render_parameters); + var me = this; + $.post(this.options.render_to, oParams, function(data){ + me.element.html(data); + me._make_draggable(); + }); + }, // Modified means: at least one change has been applied mark_as_modified: function() { @@ -130,14 +293,8 @@ $(function() me.ajax_div.html(data); }); }, - add_dashlet: function(options) + add_dashlet_ajax: function(options, sDashletId) { - var sDashletId = this._get_new_id(); - var oDashlet = $('
'); - oDashlet.appendTo(options.container); - var oDashletProperties = $('
'); - oDashletProperties.appendTo($('#dashlet_properties')); - var oParams = this.options.new_dashlet_parameters; var sDashletClass = options.dashlet_class; oParams.dashlet_class = sDashletClass; @@ -145,79 +302,17 @@ $(function() var me = this; $.post(this.options.render_to, oParams, function(data){ me.ajax_div.html(data); - $('#dashlet_'+sDashletId) - .dashlet({dashlet_id: sDashletId, dashlet_class: sDashletClass}) - .dashlet('deselect_all') - .dashlet('select') - .draggable({ - revert: 'invalid', appendTo: 'body', zIndex: 9999, - helper: function() { - var oDragItem = $(this).dashlet('get_drag_icon'); - return oDragItem; - }, - cursorAt: { top: 16, left: 16 } - }); - if (options.refresh) - { - me._refresh(); - } + me.add_dashlet_finalize(options, sDashletId, sDashletClass); }); - }, - _get_new_id: function() - { - var iMaxId = 0; - this.element.find(':itop-dashlet').each(function() { - var oDashlet = $(this).data('itopDashlet'); - if(oDashlet) - { - var oDashletParams = oDashlet.get_params(); - var id = parseInt(oDashletParams.dashlet_id, 10); - if (id > iMaxId) iMaxId = id; - } - }); - return 1 + iMaxId; - }, - _make_draggable: function() - { - var me = this; - this.element.find('.dashlet').draggable({ - revert: 'invalid', appendTo: 'body', zIndex: 9999, - helper: function() { - var oDragItem = $(this).dashlet('get_drag_icon'); - return oDragItem; - }, - cursorAt: { top: 16, left: 16 } - }); - this.element.find('table td').droppable({ - accept: '.dashlet,.dashlet_icon', - drop: function(event, ui) { - $( this ).find( ".placeholder" ).remove(); - var bRefresh = $(this).hasClass('layout_extension'); - var oDashlet = ui.draggable; - if (oDashlet.hasClass('dashlet')) - { - // moving around a dashlet - oDashlet.detach(); - oDashlet.css({top: 0, left: 0}); - oDashlet.appendTo($(this)); - if( bRefresh ) - { - // The layout was extended... refresh the whole dashboard - me._refresh(); - } - } - else - { - // inserting a new dashlet - var sDashletClass = ui.draggable.attr('dashlet_class'); - $(':itop-dashboard').dashboard('add_dashlet', {dashlet_class: sDashletClass, container: $(this), refresh: bRefresh }); - } - } - }); } }); }); + +//////////////////////////////////////////////////////////////////////////////// +// +// Helper to upload the file selected in the "import dashboard" dialog +// function UploadDashboard(oOptions) { var sFileId = 'dashboard_upload_file'; @@ -229,6 +324,10 @@ function UploadDashboard(oOptions) } +//////////////////////////////////////////////////////////////////////////////// +// +// dashboard_upload_dlg +// //jQuery UI style "widget" for managing a "import dashboard" dialog (file upload) $(function() { diff --git a/js/dashlet.js b/js/dashlet.js index cc89f127b..7e44973ee 100644 --- a/js/dashlet.js +++ b/js/dashlet.js @@ -123,8 +123,13 @@ $(function() }, _remove_dashlet: function() { - $('#dashlet_properties_'+this.options.dashlet_id).remove(); + var iDashletId = this.options.dashlet_id; + var sDashletClass = this.options.dashlet_class; + var oContainer = this.element.parent(); + + $('#dashlet_properties_'+iDashletId).remove(); this.element.remove(); + $('#event_bus').trigger('dashlet-removed', {'dashlet_id': iDashletId, 'dashlet_class': sDashletClass, 'container': oContainer}); } }); }); \ No newline at end of file diff --git a/js/property_field.js b/js/property_field.js index cc89f3656..3c2dfab9c 100644 --- a/js/property_field.js +++ b/js/property_field.js @@ -187,7 +187,7 @@ $(function() }); this.element.closest('form').find('.itop-property-field').each(function() { - var oWidget = $(this).data('property_field'); + var oWidget = $(this).data('itopProperty_field'); if (oWidget && oWidget._is_visible()) { var oVal = oWidget._get_committed_value(); diff --git a/pages/ajax.render.php b/pages/ajax.render.php index aa153d39d..15998ea55 100644 --- a/pages/ajax.render.php +++ b/pages/ajax.render.php @@ -745,12 +745,11 @@ try $sHtml = addslashes($oPage->end_capture($offset)); $sHtml = str_replace("\n", '', $sHtml); $sHtml = str_replace("\r", '', $sHtml); - - $oPage->add_script("$('#dashlet_$sDashletId').html('$sHtml')"); // in ajax web page add_script has the same effect as add_ready_script + $oPage->add_script("$('#dashlet_$sDashletId').html('$sHtml');"); // in ajax web page add_script has the same effect as add_ready_script // but is executed BEFORE all 'ready_scripts' $oForm = $oDashlet->GetForm(); // Rebuild the form since the values/content changed $oForm->SetSubmitParams(utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php', array('operation' => 'update_dashlet_property')); - $sHtml = addslashes($oForm->RenderAsPropertySheet($oPage, true /* bReturnHtml */, ':itop-dashboard')); + $sHtml = addslashes($oForm->RenderAsPropertySheet($oPage, true /* bReturnHtml */, '.itop-dashboard')); $sHtml = str_replace("\n", '', $sHtml); $sHtml = str_replace("\r", '', $sHtml); $oPage->add_script("$('#dashlet_properties_$sDashletId').html('$sHtml')"); // in ajax web page add_script has the same effect as add_ready_script // but is executed BEFORE all 'ready_scripts' @@ -803,7 +802,7 @@ try { $oForm = $oDashlet->GetForm(); // Rebuild the form since the values/content changed $oForm->SetSubmitParams(utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php', array('operation' => 'update_dashlet_property')); - $sHtml = addslashes($oForm->RenderAsPropertySheet($oPage, true /* bReturnHtml */, ':itop-dashboard')); + $sHtml = addslashes($oForm->RenderAsPropertySheet($oPage, true /* bReturnHtml */, '.itop-dashboard')); $sHtml = str_replace("\n", '', $sHtml); $sHtml = str_replace("\r", '', $sHtml); $oPage->add_script("$('#dashlet_properties_$sDashletId').html('$sHtml')"); // in ajax web page add_script has the same effect as add_ready_script // but is executed BEFORE all 'ready_scripts' diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php index 1d6f7703b..43dc20b10 100644 --- a/setup/compiler.class.inc.php +++ b/setup/compiler.class.inc.php @@ -567,42 +567,13 @@ EOF; $aClassParams['display_template'] = "utils::GetAbsoluteUrlModulesRoot().'$sDisplayTemplate'"; } + $this->CompileFiles($oProperties, $sTargetDir.'/'.$sModuleRelativeDir, ''); if (($sIcon = $oProperties->GetChildText('icon')) && (strlen($sIcon) > 0)) { $sIcon = $sModuleRelativeDir.'/'.$sIcon; $aClassParams['icon'] = "utils::GetAbsoluteUrlModulesRoot().'$sIcon'"; } - else // si - { - $oIcon = $oProperties->GetOptionalElement('icon'); - if ($oIcon) - { - $oFileRef = $oIcon->GetOptionalElement('fileref'); - if ($oFileRef) - { - $iFileId = $oFileRef->getAttribute('ref'); - $sXPath = "/itop_design/files/file[@id='$iFileId']"; - $oNodes = $this->oFactory->GetNodes($sXPath); - if ($oNodes->length == 0) - { - throw new DOMFormatException('Could not find the file with ref '.$iFileId); - } - $sName = $oNodes->item(0)->GetChildText('name'); - $sData = base64_decode($oNodes->item(0)->GetChildText('data')); - $aPathInfo = pathinfo($sName); - $sFile = 'icon-file'.$iFileId.'.'.$aPathInfo['extension']; - $sFilePath = $sTargetDir.'/'.$sModuleRelativeDir.'/'.$sFile; - file_put_contents($sFilePath, $sData); - if (!file_exists($sFilePath)) - { - throw new Exception('Could not write icon file '.$sFilePath); - } - $aClassParams['icon'] = "utils::GetAbsoluteUrlModulesRoot().'$sModuleRelativeDir/$sFile'"; - } - } - } - $oOrder = $oProperties->GetOptionalElement('order'); if ($oOrder) { @@ -1028,6 +999,8 @@ EOF; protected function CompileMenu($oMenu, $sTargetDir, $sModuleRelativeDir, $oP) { + $this->CompileFiles($oMenu, $sTargetDir.'/'.$sModuleRelativeDir, $sModuleRelativeDir); + $sMenuId = $oMenu->getAttribute("id"); $sMenuClass = $oMenu->getAttribute("xsi:type"); @@ -1065,11 +1038,11 @@ EOF; } $sFileName = strtolower(str_replace(array(':', '/', '\\', '*'), '_', $sMenuId)).'_dashboard_menu.xml'; $sTemplateSpec = $this->PathToPHP($sFileName, $sModuleRelativeDir); - + $oXMLDoc = new DOMDocument('1.0', 'UTF-8'); $oXMLDoc->formatOutput = true; // indent (must be loaded with option LIBXML_NOBLANKS) $oXMLDoc->preserveWhiteSpace = true; // otherwise the formatOutput option would have no effect - + $oRootNode = $oXMLDoc->createElement('dashboard'); // make sure that the document is not empty $oRootNode->setAttribute('xmlns:xsi', "http://www.w3.org/2001/XMLSchema-instance"); $oXMLDoc->appendChild($oRootNode); @@ -1325,6 +1298,11 @@ class ProfilesConfig protected static \$aLINKTOCLASSES = $sLinkToClasses; + public static function GetLinkClasses() + { + return self::\$aLINKTOCLASSES; + } + public static function GetProfileActionGrant(\$iProfileId, \$sClass, \$sAction) { // Search for a grant, starting from the most explicit declaration, @@ -1462,6 +1440,42 @@ EOF; $sDictFile = $sTargetDir.'/dictionaries/'.$sSafeLang.'.dict.php'; file_put_contents($sDictFile, $sPHPDict); } + + // Transform the file references into the corresponding filename (and create the file in the relevant directory) + // + protected function CompileFiles($oNode, $sTargetDir, $sRelativePath) + { + $oFileRefs = $oNode->GetNodes(".//fileref"); + foreach ($oFileRefs as $oFileRef) + { + $iFileId = $oFileRef->getAttribute('ref'); + if ($iFileId > 0) + { + $oNodes = $this->oFactory->GetNodes("/itop_design/files/file[@id='$iFileId']"); + if ($oNodes->length == 0) + { + throw new DOMFormatException('Could not find the file with ref '.$iFileId); + } + + $sName = $oNodes->item(0)->GetChildText('name'); + $sData = base64_decode($oNodes->item(0)->GetChildText('data')); + $aPathInfo = pathinfo($sName); + $sFile = 'icon-file'.$iFileId.'.'.$aPathInfo['extension']; + $sFilePath = $sTargetDir.'/images/'.$sFile; + @mkdir($sTargetDir.'/images'); + file_put_contents($sFilePath, $sData); + if (!file_exists($sFilePath)) + { + throw new Exception('Could not write icon file '.$sFilePath); + } + $oParentNode = $oFileRef->parentNode; + $oParentNode->removeChild($oFileRef); + + $oTextNode = $oParentNode->ownerDocument->createTextNode($sRelativePath.'/images/'.$sFile); + $oParentNode->appendChild($oTextNode); + } + } + } } ?> \ No newline at end of file diff --git a/setup/modelfactory.class.inc.php b/setup/modelfactory.class.inc.php index d46ed45a9..e6814a428 100644 --- a/setup/modelfactory.class.inc.php +++ b/setup/modelfactory.class.inc.php @@ -1440,12 +1440,26 @@ class MFElement extends DOMElement return $this->ownerDocument->GetNodes($sXPath, $this); } + /** + * Extracts some nodes from the DOM (active nodes only !!!) + * @param string $sXPath A XPath expression + * @return DOMNodeList + */ + public function GetNodeById($sXPath, $sId) + { + return $this->ownerDocument->GetNodeById($sXPath, $sId, $this); + } + /** * For debugging purposes - but this is currently buggy: the whole document is rendered */ public function Dump($bReturnRes = false) { - $sXml = $this->ownerDocument->saveXML($this); + $oMFDoc = new MFDocument(); + $oClone = $oMFDoc->importNode($this->cloneNode(true), true); + $oMFDoc->appendChild($oClone); + + $sXml = $oMFDoc->saveXML($oClone); if ($bReturnRes) { return $sXml; @@ -1713,6 +1727,9 @@ class MFElement extends DOMElement */ public function AddChildNode(MFElement $oNode) { + // First: cleanup any flag behind the new node + $oNode->ApplyChanges(); + $oExisting = $this->FindExistingChildNode($oNode); if ($oExisting) { @@ -1740,6 +1757,9 @@ class MFElement extends DOMElement */ public function RedefineChildNode(MFElement $oNode, $sSearchId = null) { + // First: cleanup any flag behind the new node + $oNode->ApplyChanges(); + $oExisting = $this->FindExistingChildNode($oNode, $sSearchId); if (!$oExisting) { @@ -1906,7 +1926,45 @@ class MFElement extends DOMElement $aCurrentRules = $oRulesNode->GetNodeAsArrayOfItems(); } return $aCurrentRules; - } + } + + /** + * List changes below a given node (see also MFDocument::ListChanges) + */ + public function ListChanges() + { + // Note: omitting the dot will make the query be global to the whole document!!! + return $this->GetNodes('.//*[@_alteration or @_old_id]'); + } + + /** + * List changes below a given node (see also MFDocument::ApplyChanges) + */ + public function ApplyChanges() + { + $oNodes = $this->ListChanges(); + foreach($oNodes as $oNode) + { + $sOperation = $oNode->GetAttribute('_alteration'); + switch($sOperation) + { + case 'added': + case 'replaced': + // marked as added or modified, just reset the flag + $oNode->removeAttribute('_alteration'); + break; + + case 'removed': + // marked as deleted, let's remove the node from the tree + $oNode->parentNode->removeChild($oNode); + break; + } + if ($oNode->hasAttribute('_old_id')) + { + $oNode->removeAttribute('_old_id'); + } + } + } } /**