From 3c80c93b4ff9315a3fd788e8b89460e2aca7f72c Mon Sep 17 00:00:00 2001 From: Eric Espie Date: Fri, 16 Jan 2026 18:47:28 +0100 Subject: [PATCH] Dashlet extraction --- application/dashboard.class.inc.php | 56 +- application/dashboardlayout.class.inc.php | 20 +- application/datamodel.application.xml | 36 + git-split.md | 17 + lib/composer/autoload_classmap.php | 32 +- lib/composer/autoload_static.php | 32 +- setup/compiler.class.inc.php | 13 + .../Controller}/DashboardController.php | 22 +- .../Dashboard/DashboardException.php | 12 + .../Dashboard/Layout/DashboardLayoutGrid.php | 5 +- .../Application/Dashlet/Core/DashletBadge.php | 126 + .../Dashlet/Core/DashletGroupBy.php | 590 ++++ .../Dashlet/Core/DashletGroupByBars.php | 112 + .../Dashlet/Core/DashletGroupByPie.php | 109 + .../Dashlet/Core/DashletGroupByTable.php | 79 + .../Dashlet/Core/DashletHeaderDynamic.php | 326 +++ .../Dashlet/Core/DashletHeaderStatic.php | 98 + .../Dashlet/Core/DashletObjectList.php | 176 ++ .../Dashlet/Core/DashletPlainText.php | 63 + .../Application/Dashlet/Core/DashletProxy.php | 75 + .../Dashlet/Core/DashletUnknown.php | 227 ++ sources/Application/Dashlet/Dashlet.php | 517 ++++ .../Application/Dashlet/DashletException.php | 12 + .../Application/Dashlet/DashletFactory.php | 49 + .../Dashlet/Service/DashletService.php | 143 + .../Application/Dashlet/dashlet.class.inc.php | 2397 ----------------- .../DashletPanel/DashletPanelFactory.php | 4 +- sources/Service/Dashboard/DashletService.php | 31 - sources/alias.php | 13 + 29 files changed, 2871 insertions(+), 2521 deletions(-) create mode 100644 git-split.md rename sources/{Controller/Base/Layout => Application/Dashboard/Controller}/DashboardController.php (91%) create mode 100644 sources/Application/Dashboard/DashboardException.php create mode 100644 sources/Application/Dashlet/Core/DashletBadge.php create mode 100644 sources/Application/Dashlet/Core/DashletGroupBy.php create mode 100644 sources/Application/Dashlet/Core/DashletGroupByBars.php create mode 100644 sources/Application/Dashlet/Core/DashletGroupByPie.php create mode 100644 sources/Application/Dashlet/Core/DashletGroupByTable.php create mode 100644 sources/Application/Dashlet/Core/DashletHeaderDynamic.php create mode 100644 sources/Application/Dashlet/Core/DashletHeaderStatic.php create mode 100644 sources/Application/Dashlet/Core/DashletObjectList.php create mode 100644 sources/Application/Dashlet/Core/DashletPlainText.php create mode 100644 sources/Application/Dashlet/Core/DashletProxy.php create mode 100644 sources/Application/Dashlet/Core/DashletUnknown.php create mode 100644 sources/Application/Dashlet/Dashlet.php create mode 100644 sources/Application/Dashlet/DashletException.php create mode 100644 sources/Application/Dashlet/DashletFactory.php create mode 100644 sources/Application/Dashlet/Service/DashletService.php delete mode 100644 sources/Application/Dashlet/dashlet.class.inc.php delete mode 100644 sources/Service/Dashboard/DashletService.php diff --git a/application/dashboard.class.inc.php b/application/dashboard.class.inc.php index 7bece1104..beccb856f 100644 --- a/application/dashboard.class.inc.php +++ b/application/dashboard.class.inc.php @@ -6,6 +6,8 @@ */ use Combodo\iTop\Application\Dashboard\Layout\DashboardLayoutGrid; +use Combodo\iTop\Application\Dashlet\DashletFactory; +use Combodo\iTop\Application\Dashlet\Service\DashletService; use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\DataTable\DataTableSettings; use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenu; @@ -19,7 +21,6 @@ use Combodo\iTop\PropertyType\PropertyTypeDesign; use Combodo\iTop\Service\DependencyInjection\ServiceLocator; require_once(APPROOT.'application/dashboardlayout.class.inc.php'); -require_once(APPROOT.'application/dashlet.class.inc.php'); require_once(APPROOT.'core/modelreflection.class.inc.php'); /** @@ -51,6 +52,8 @@ abstract class Dashboard /** @var array Array of dashlets with position */ protected array $aGridDashlets = []; + protected $oDashletFactory; + /** * Dashboard constructor. * @@ -65,6 +68,7 @@ abstract class Dashboard $this->aCells = []; $this->oDOMNode = null; $this->sId = $sId; + $this->oDashletFactory = DashletFactory::GetInstance(); } /** @@ -192,7 +196,7 @@ abstract class Dashboard } /** - * @param \DOMElement $oDomNode + * @param DesignElement $oDomNode * * @return mixed */ @@ -205,7 +209,7 @@ abstract class Dashboard // Test if dashlet can be instantiated, otherwise (uninstalled, broken, ...) we display a placeholder $sClass = static::GetDashletClassFromType($sDashletType); /** @var \Dashlet $oNewDashlet */ - $oNewDashlet = new $sClass($this->oMetaModel, $sId); + $oNewDashlet = $this->oDashletFactory->CreateDashlet($sClass, $sId); $oNewDashlet->SetDashletType($sDashletType); $oNewDashlet->FromDOMNode($oDomNode); @@ -345,7 +349,7 @@ abstract class Dashboard $sDashletClass = $aDashletParams['dashlet_class']; $sId = $aDashletParams['dashlet_id']; /** @var \Dashlet $oNewDashlet */ - $oNewDashlet = new $sDashletClass($this->oMetaModel, $sId); + $oNewDashlet = $this->oDashletFactory->CreateDashlet($sDashletClass, $sId); if (isset($aDashletParams['dashlet_type'])) { $oNewDashlet->SetDashletType($aDashletParams['dashlet_type']); } @@ -671,29 +675,12 @@ JS * Return an array of dashlets available for selection. * * @return array - * @throws \ReflectionException + * @throws \Combodo\iTop\Application\Dashlet\DashletException + * @throws \DOMFormatException */ - protected function GetAvailableDashlets() + protected function GetAvailableDashlets(): array { - $aDashlets = []; - - foreach (get_declared_classes() as $sDashletClass) { - // DashletUnknown is not among the selection as it is just a fallback for dashlets that can't instantiated. - if (is_subclass_of($sDashletClass, 'Dashlet') && !in_array($sDashletClass, ['DashletUnknown', 'DashletProxy'])) { - $oReflection = new ReflectionClass($sDashletClass); - if (!$oReflection->isAbstract()) { - $aCallSpec = [$sDashletClass, 'IsVisible']; - $bVisible = call_user_func($aCallSpec); - if ($bVisible) { - $aCallSpec = [$sDashletClass, 'GetInfo']; - $aInfo = call_user_func($aCallSpec); - $aDashlets[$sDashletClass] = $aInfo; - } - } - } - } - - return $aDashlets; + return DashletService::GetInstance()->GetAvailableDashlets(); } /** @@ -796,6 +783,7 @@ class RuntimeDashboard extends Dashboard { parent::__construct($sId); $this->oMetaModel = new ModelReflectionRuntime(); + $this->oDashletFactory->SetModelReflectionRuntime($this->oMetaModel); ServiceLocator::GetInstance()->RegisterService('ModelReflection', $this->oMetaModel); $this->bCustomized = false; } @@ -1504,19 +1492,11 @@ JS // Get the list of possible dashlets that support a creation from // an OQL + $aAllDashlets = DashletService::GetInstance()->GetAvailableDashlets(); $aDashlets = []; - foreach (get_declared_classes() as $sDashletClass) { - if (is_subclass_of($sDashletClass, 'Dashlet')) { - $oReflection = new ReflectionClass($sDashletClass); - if (!$oReflection->isAbstract()) { - $aCallSpec = [$sDashletClass, 'CanCreateFromOQL']; - $bShorcutMode = call_user_func($aCallSpec); - if ($bShorcutMode) { - $aCallSpec = [$sDashletClass, 'GetInfo']; - $aInfo = call_user_func($aCallSpec); - $aDashlets[$sDashletClass] = ['label' => $aInfo['label'], 'class' => $sDashletClass, 'icon' => $aInfo['icon']]; - } - } + foreach ($aAllDashlets as $sDashletClass => $aInfo) { + if ($aInfo['can_create_by_oql']) { + $aDashlets[$sDashletClass] = ['label' => $aInfo['label'], 'class' => $sDashletClass, 'icon' => $aInfo['icon']]; } } @@ -1526,7 +1506,7 @@ JS $oSubForm = new DesignerForm(); $oMetaModel = new ModelReflectionRuntime(); /** @var \Dashlet $oDashlet */ - $oDashlet = new $sDashletClass($oMetaModel, 0); + $oDashlet = DashletFactory::GetInstance()->CreateDashlet($sDashletClass, 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 607c88795..97baeb149 100644 --- a/application/dashboardlayout.class.inc.php +++ b/application/dashboardlayout.class.inc.php @@ -140,16 +140,16 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout $sDashletId = $oDashlet->GetID(); $sDashletClass = get_class($oDashlet); $aDashletDenormalizedProperties = $oDashlet->GetDenormalizedProperties(); - $aDashletsInfo = $sDashletClass::GetInfo(); - - // TODO 3.3 Gather real position and height/width if any. - // Also set minimal height/width - $iPositionX = null; - $iPositionY = null; - $iWidth = array_key_exists('preferred_width', $aDashletsInfo) ? $aDashletsInfo['preferred_width'] : 1; - $iHeight = array_key_exists('preferred_height', $aDashletsInfo) ? $aDashletsInfo['preferred_height'] : 1; - $oDashboardGrid->AddDashlet($oDashlet->DoRender($oPage, $bEditMode, true /* bEnclosingDiv */, $aExtraParams), $sDashletId, $sDashletClass, $aDashletDenormalizedProperties, $iPositionX, $iPositionY, $iWidth, $iHeight); - //$oDashboardColumn->AddUIBlock($oDashlet->DoRender($oPage, $bEditMode, true /* bEnclosingDiv */, $aExtraParams)); + // $aDashletsInfo = $sDashletClass::GetInfo(); + // + // // TODO 3.3 Gather real position and height/width if any. + // // Also set minimal height/width + // $iPositionX = null; + // $iPositionY = null; + // $iWidth = array_key_exists('preferred_width', $aDashletsInfo) ? $aDashletsInfo['preferred_width'] : 1; + // $iHeight = array_key_exists('preferred_height', $aDashletsInfo) ? $aDashletsInfo['preferred_height'] : 1; + // $oDashboardGrid->AddDashlet($oDashlet->DoRender($oPage, $bEditMode, true /* bEnclosingDiv */, $aExtraParams), $sDashletId, $sDashletClass, $aDashletDenormalizedProperties, $iPositionX, $iPositionY, $iWidth, $iHeight); + $oDashboardColumn->AddUIBlock($oDashlet->DoRender($oPage, $bEditMode, true /* bEnclosingDiv */, $aExtraParams)); } } } else { diff --git a/application/datamodel.application.xml b/application/datamodel.application.xml index 29bf3f4ca..ab70e7faa 100644 --- a/application/datamodel.application.xml +++ b/application/datamodel.application.xml @@ -856,48 +856,84 @@ Call $this->AddInitialAttributeFlags($sAttCode, $iFlags) for all the initial att images/dashlets/icons8-transaction-list-48.png UI:DashletGroupByTable:Description + 2 + 2 + 3 + 3 + true images/dashlets/icons8-bar-chart-48.png UI:DashletGroupByBars:Description + 2 + 2 + 3 + 3 + true images/dashlets/icons8-pie-chart-48.png UI:DashletGroupByPie:Description + 2 + 2 + 3 + 3 + true images/dashlets/icons8-badge-48.png UI:DashletBadge:Description + 2 + 1 + 2 + 1 images/dashlets/icons8-header-altered-48.png UI:DashletHeaderDynamic:Description + 2 + 1 + 4 + 3 images/dashlets/icons8-header-48.png UI:DashletHeaderStatic:Description + 4 + 1 + 4 + 1 images/dashlets/icons8-list-48.png UI:DashletObjectList:Description + 2 + 1 + 4 + 3 + true images/dashlets/icons8-text-box-48.png UI:DashletPlainText:Description + 2 + 1 + 2 + 1 diff --git a/git-split.md b/git-split.md new file mode 100644 index 000000000..72ad00254 --- /dev/null +++ b/git-split.md @@ -0,0 +1,17 @@ +git copy of one file into two files without loosing history + +``` +git mv foo bar +git commit + +SAVED=`git rev-parse HEAD` +git reset --hard HEAD^ +git mv foo copy +git commit + +git merge $SAVED # This will generate conflicts +git commit -a # Trivially resolved like this + +git mv copy foo +git commit +``` \ No newline at end of file diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index b8ec6b067..0dd74e24b 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -130,10 +130,27 @@ return array( 'CheckableExpression' => $baseDir . '/core/oql/oqlquery.class.inc.php', 'Collator' => $vendorDir . '/symfony/polyfill-intl-icu/Resources/stubs/Collator.php', 'Combodo\\iTop\\Application\\Branding' => $baseDir . '/sources/Application/Branding.php', + 'Combodo\\iTop\\Application\\Dashboard\\Controller\\DashboardController' => $baseDir . '/sources/Application/Dashboard/Controller/DashboardController.php', + 'Combodo\\iTop\\Application\\Dashboard\\DashboardException' => $baseDir . '/sources/Application/Dashboard/DashboardException.php', 'Combodo\\iTop\\Application\\Dashboard\\FormBlock\\DashboardFormBlock' => $baseDir . '/sources/Application/Dashboard/FormBlock/DashboardFormBlock.php', 'Combodo\\iTop\\Application\\Dashboard\\FormBlock\\DashletFormBlock' => $baseDir . '/sources/Application/Dashboard/FormBlock/DashletFormBlock.php', 'Combodo\\iTop\\Application\\Dashboard\\FormBlock\\DashletPropertiesFormBlock' => $baseDir . '/sources/Application/Dashboard/FormBlock/DashletPropertiesFormBlock.php', 'Combodo\\iTop\\Application\\Dashboard\\Layout\\DashboardLayoutGrid' => $baseDir . '/sources/Application/Dashboard/Layout/DashboardLayoutGrid.php', + 'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletBadge' => $baseDir . '/sources/Application/Dashlet/Core/DashletBadge.php', + 'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletGroupBy' => $baseDir . '/sources/Application/Dashlet/Core/DashletGroupBy.php', + 'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletGroupByBars' => $baseDir . '/sources/Application/Dashlet/Core/DashletGroupByBars.php', + 'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletGroupByPie' => $baseDir . '/sources/Application/Dashlet/Core/DashletGroupByPie.php', + 'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletGroupByTable' => $baseDir . '/sources/Application/Dashlet/Core/DashletGroupByTable.php', + 'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletHeaderDynamic' => $baseDir . '/sources/Application/Dashlet/Core/DashletHeaderDynamic.php', + 'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletHeaderStatic' => $baseDir . '/sources/Application/Dashlet/Core/DashletHeaderStatic.php', + 'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletObjectList' => $baseDir . '/sources/Application/Dashlet/Core/DashletObjectList.php', + 'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletPlainText' => $baseDir . '/sources/Application/Dashlet/Core/DashletPlainText.php', + 'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletProxy' => $baseDir . '/sources/Application/Dashlet/Core/DashletProxy.php', + 'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletUnknown' => $baseDir . '/sources/Application/Dashlet/Core/DashletUnknown.php', + 'Combodo\\iTop\\Application\\Dashlet\\Dashlet' => $baseDir . '/sources/Application/Dashlet/Dashlet.php', + 'Combodo\\iTop\\Application\\Dashlet\\DashletException' => $baseDir . '/sources/Application/Dashlet/DashletException.php', + 'Combodo\\iTop\\Application\\Dashlet\\DashletFactory' => $baseDir . '/sources/Application/Dashlet/DashletFactory.php', + 'Combodo\\iTop\\Application\\Dashlet\\Service\\DashletService' => $baseDir . '/sources/Application/Dashlet/Service/DashletService.php', 'Combodo\\iTop\\Application\\EventRegister\\ApplicationEvents' => $baseDir . '/sources/Application/EventRegister/ApplicationEvents.php', 'Combodo\\iTop\\Application\\Helper\\CKEditorHelper' => $baseDir . '/sources/Application/Helper/CKEditorHelper.php', 'Combodo\\iTop\\Application\\Helper\\ExportHelper' => $baseDir . '/sources/Application/Helper/ExportHelper.php', @@ -354,7 +371,6 @@ return array( 'Combodo\\iTop\\Controller\\AbstractController' => $baseDir . '/sources/Controller/AbstractController.php', 'Combodo\\iTop\\Controller\\AjaxRenderController' => $baseDir . '/sources/Controller/AjaxRenderController.php', 'Combodo\\iTop\\Controller\\Base\\Layout\\ActivityPanelController' => $baseDir . '/sources/Controller/Base/Layout/ActivityPanelController.php', - 'Combodo\\iTop\\Controller\\Base\\Layout\\DashboardController' => $baseDir . '/sources/Controller/Base/Layout/DashboardController.php', 'Combodo\\iTop\\Controller\\Base\\Layout\\ObjectController' => $baseDir . '/sources/Controller/Base/Layout/ObjectController.php', 'Combodo\\iTop\\Controller\\Base\\Layout\\OqlController' => $baseDir . '/sources/Controller/Base/Layout/OqlController.php', 'Combodo\\iTop\\Controller\\Links\\LinkSetController' => $baseDir . '/sources/Controller/Links/LinkSetController.php', @@ -624,7 +640,6 @@ return array( 'Combodo\\iTop\\Service\\Base\\ObjectRepository' => $baseDir . '/sources/Service/Base/ObjectRepository.php', 'Combodo\\iTop\\Service\\Base\\iDataPostProcessor' => $baseDir . '/sources/Service/Base/iDataPostProcessor.php', 'Combodo\\iTop\\Service\\Cache\\DataModelDependantCache' => $baseDir . '/sources/Service/Cache/DataModelDependantCache.php', - 'Combodo\\iTop\\Service\\Dashboard\\DashletService' => $baseDir . '/sources/Service/Dashboard/DashletService.php', 'Combodo\\iTop\\Service\\DependencyInjection\\DIException' => $baseDir . '/sources/Service/DependencyInjection/DIException.php', 'Combodo\\iTop\\Service\\DependencyInjection\\ServiceLocator' => $baseDir . '/sources/Service/DependencyInjection/ServiceLocator.php', 'Combodo\\iTop\\Service\\Events\\Description\\EventDataDescription' => $baseDir . '/sources/Service/Events/Description/EventDataDescription.php', @@ -694,19 +709,6 @@ return array( 'DashboardLayoutThreeCols' => $baseDir . '/application/dashboardlayout.class.inc.php', 'DashboardLayoutTwoCols' => $baseDir . '/application/dashboardlayout.class.inc.php', 'DashboardMenuNode' => $baseDir . '/application/menunode.class.inc.php', - 'Dashlet' => $baseDir . '/application/dashlet.class.inc.php', - 'DashletBadge' => $baseDir . '/application/dashlet.class.inc.php', - 'DashletEmptyCell' => $baseDir . '/application/dashlet.class.inc.php', - 'DashletGroupBy' => $baseDir . '/application/dashlet.class.inc.php', - 'DashletGroupByBars' => $baseDir . '/application/dashlet.class.inc.php', - 'DashletGroupByPie' => $baseDir . '/application/dashlet.class.inc.php', - 'DashletGroupByTable' => $baseDir . '/application/dashlet.class.inc.php', - 'DashletHeaderDynamic' => $baseDir . '/application/dashlet.class.inc.php', - 'DashletHeaderStatic' => $baseDir . '/application/dashlet.class.inc.php', - 'DashletObjectList' => $baseDir . '/application/dashlet.class.inc.php', - 'DashletPlainText' => $baseDir . '/application/dashlet.class.inc.php', - 'DashletProxy' => $baseDir . '/application/dashlet.class.inc.php', - 'DashletUnknown' => $baseDir . '/application/dashlet.class.inc.php', 'Datamatrix' => $vendorDir . '/tecnickcom/tcpdf/include/barcodes/datamatrix.php', 'DateError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateError.php', 'DateException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateException.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 5f6d058e2..d8c3c5689 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -516,10 +516,27 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'CheckableExpression' => __DIR__ . '/../..' . '/core/oql/oqlquery.class.inc.php', 'Collator' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/Resources/stubs/Collator.php', 'Combodo\\iTop\\Application\\Branding' => __DIR__ . '/../..' . '/sources/Application/Branding.php', + 'Combodo\\iTop\\Application\\Dashboard\\Controller\\DashboardController' => __DIR__ . '/../..' . '/sources/Application/Dashboard/Controller/DashboardController.php', + 'Combodo\\iTop\\Application\\Dashboard\\DashboardException' => __DIR__ . '/../..' . '/sources/Application/Dashboard/DashboardException.php', 'Combodo\\iTop\\Application\\Dashboard\\FormBlock\\DashboardFormBlock' => __DIR__ . '/../..' . '/sources/Application/Dashboard/FormBlock/DashboardFormBlock.php', 'Combodo\\iTop\\Application\\Dashboard\\FormBlock\\DashletFormBlock' => __DIR__ . '/../..' . '/sources/Application/Dashboard/FormBlock/DashletFormBlock.php', 'Combodo\\iTop\\Application\\Dashboard\\FormBlock\\DashletPropertiesFormBlock' => __DIR__ . '/../..' . '/sources/Application/Dashboard/FormBlock/DashletPropertiesFormBlock.php', 'Combodo\\iTop\\Application\\Dashboard\\Layout\\DashboardLayoutGrid' => __DIR__ . '/../..' . '/sources/Application/Dashboard/Layout/DashboardLayoutGrid.php', + 'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletBadge' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Core/DashletBadge.php', + 'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletGroupBy' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Core/DashletGroupBy.php', + 'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletGroupByBars' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Core/DashletGroupByBars.php', + 'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletGroupByPie' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Core/DashletGroupByPie.php', + 'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletGroupByTable' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Core/DashletGroupByTable.php', + 'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletHeaderDynamic' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Core/DashletHeaderDynamic.php', + 'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletHeaderStatic' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Core/DashletHeaderStatic.php', + 'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletObjectList' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Core/DashletObjectList.php', + 'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletPlainText' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Core/DashletPlainText.php', + 'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletProxy' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Core/DashletProxy.php', + 'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletUnknown' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Core/DashletUnknown.php', + 'Combodo\\iTop\\Application\\Dashlet\\Dashlet' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Dashlet.php', + 'Combodo\\iTop\\Application\\Dashlet\\DashletException' => __DIR__ . '/../..' . '/sources/Application/Dashlet/DashletException.php', + 'Combodo\\iTop\\Application\\Dashlet\\DashletFactory' => __DIR__ . '/../..' . '/sources/Application/Dashlet/DashletFactory.php', + 'Combodo\\iTop\\Application\\Dashlet\\Service\\DashletService' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Service/DashletService.php', 'Combodo\\iTop\\Application\\EventRegister\\ApplicationEvents' => __DIR__ . '/../..' . '/sources/Application/EventRegister/ApplicationEvents.php', 'Combodo\\iTop\\Application\\Helper\\CKEditorHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/CKEditorHelper.php', 'Combodo\\iTop\\Application\\Helper\\ExportHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/ExportHelper.php', @@ -740,7 +757,6 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Combodo\\iTop\\Controller\\AbstractController' => __DIR__ . '/../..' . '/sources/Controller/AbstractController.php', 'Combodo\\iTop\\Controller\\AjaxRenderController' => __DIR__ . '/../..' . '/sources/Controller/AjaxRenderController.php', 'Combodo\\iTop\\Controller\\Base\\Layout\\ActivityPanelController' => __DIR__ . '/../..' . '/sources/Controller/Base/Layout/ActivityPanelController.php', - 'Combodo\\iTop\\Controller\\Base\\Layout\\DashboardController' => __DIR__ . '/../..' . '/sources/Controller/Base/Layout/DashboardController.php', 'Combodo\\iTop\\Controller\\Base\\Layout\\ObjectController' => __DIR__ . '/../..' . '/sources/Controller/Base/Layout/ObjectController.php', 'Combodo\\iTop\\Controller\\Base\\Layout\\OqlController' => __DIR__ . '/../..' . '/sources/Controller/Base/Layout/OqlController.php', 'Combodo\\iTop\\Controller\\Links\\LinkSetController' => __DIR__ . '/../..' . '/sources/Controller/Links/LinkSetController.php', @@ -1010,7 +1026,6 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Combodo\\iTop\\Service\\Base\\ObjectRepository' => __DIR__ . '/../..' . '/sources/Service/Base/ObjectRepository.php', 'Combodo\\iTop\\Service\\Base\\iDataPostProcessor' => __DIR__ . '/../..' . '/sources/Service/Base/iDataPostProcessor.php', 'Combodo\\iTop\\Service\\Cache\\DataModelDependantCache' => __DIR__ . '/../..' . '/sources/Service/Cache/DataModelDependantCache.php', - 'Combodo\\iTop\\Service\\Dashboard\\DashletService' => __DIR__ . '/../..' . '/sources/Service/Dashboard/DashletService.php', 'Combodo\\iTop\\Service\\DependencyInjection\\DIException' => __DIR__ . '/../..' . '/sources/Service/DependencyInjection/DIException.php', 'Combodo\\iTop\\Service\\DependencyInjection\\ServiceLocator' => __DIR__ . '/../..' . '/sources/Service/DependencyInjection/ServiceLocator.php', 'Combodo\\iTop\\Service\\Events\\Description\\EventDataDescription' => __DIR__ . '/../..' . '/sources/Service/Events/Description/EventDataDescription.php', @@ -1080,19 +1095,6 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'DashboardLayoutThreeCols' => __DIR__ . '/../..' . '/application/dashboardlayout.class.inc.php', 'DashboardLayoutTwoCols' => __DIR__ . '/../..' . '/application/dashboardlayout.class.inc.php', 'DashboardMenuNode' => __DIR__ . '/../..' . '/application/menunode.class.inc.php', - 'Dashlet' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php', - 'DashletBadge' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php', - 'DashletEmptyCell' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php', - 'DashletGroupBy' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php', - 'DashletGroupByBars' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php', - 'DashletGroupByPie' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php', - 'DashletGroupByTable' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php', - 'DashletHeaderDynamic' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php', - 'DashletHeaderStatic' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php', - 'DashletObjectList' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php', - 'DashletPlainText' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php', - 'DashletProxy' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php', - 'DashletUnknown' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php', 'Datamatrix' => __DIR__ . '/..' . '/tecnickcom/tcpdf/include/barcodes/datamatrix.php', 'DateError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateError.php', 'DateException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateException.php', diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php index a9366a663..ce8b696bf 100644 --- a/setup/compiler.class.inc.php +++ b/setup/compiler.class.inc.php @@ -704,6 +704,9 @@ PHP; $oPropertyTypesNode = $this->oFactory->GetNodes('/itop_design/meta/property_types')->item(0); $this->CompilePropertyTypes($oPropertyTypesNode, $sTempTargetDir, $sFinalTargetDir); + $oDashletNode = $this->oFactory->GetNodes('/itop_design/meta/dashlets')->item(0); + $this->CompileDashlets($oDashletNode, $sTempTargetDir, $sFinalTargetDir); + // Compile the XML parameters /** @var \MFElement $oParametersNode */ $oParametersNode = $this->oFactory->GetNodes('/itop_design/module_parameters')->item(0); @@ -3592,6 +3595,16 @@ EOF; } } + protected function CompileDashlets(?DOMNode $oDashlets, string $sTempTargetDir, string $sFinalTargetDir): void + { + if ($oDashlets) { + $oDoc = new DesignDocument(); + $oClone = $oDoc->importNode($oDashlets->cloneNode(true), true); + $oDoc->appendChild($oClone); + $oDoc->save($sTempTargetDir.'/core/dashlets.xml'); + } + } + /** * @throws \DOMFormatException */ diff --git a/sources/Controller/Base/Layout/DashboardController.php b/sources/Application/Dashboard/Controller/DashboardController.php similarity index 91% rename from sources/Controller/Base/Layout/DashboardController.php rename to sources/Application/Dashboard/Controller/DashboardController.php index 862b022e8..77e7187a8 100644 --- a/sources/Controller/Base/Layout/DashboardController.php +++ b/sources/Application/Dashboard/Controller/DashboardController.php @@ -1,23 +1,24 @@ CreateDashlet($sDashletClass, $sDashletId); // TODO 3.3 This is not the place to register this service, do better please ServiceLocator::GetInstance()->RegisterService('ModelReflection', new ModelReflectionRuntime()); @@ -113,8 +114,7 @@ class DashboardController extends Controller $oDashboard->PersistDashboard($sXml); $sStatus = 'ok'; $sMessage = 'Dashboard saved'; - } - else { + } else { $sStatus = 'error'; $aFormErrors = $oForm->getErrors(true, true); $sMessage = $aFormErrors->__toString(); @@ -128,7 +128,7 @@ class DashboardController extends Controller $oPage = new JsonPage(); $oPage->SetData([ 'status' => $sStatus, - 'message' => $sMessage + 'message' => $sMessage, ]); $oPage->SetOutputDataOnly(true); return $oPage; diff --git a/sources/Application/Dashboard/DashboardException.php b/sources/Application/Dashboard/DashboardException.php new file mode 100644 index 000000000..8e3837fa9 --- /dev/null +++ b/sources/Application/Dashboard/DashboardException.php @@ -0,0 +1,12 @@ +GetID(); - $sDashletClass = get_class($oDashlet); + $sDashletClass = $oDashlet->GetDashletType(); $aDashletDenormalizedProperties = $oDashlet->GetDenormalizedProperties(); - $aDashletsInfo = $sDashletClass::GetInfo(); + $aDashletsInfo = DashletService::GetInstance()->GetDashletDefinition($sDashletClass); // Also set minimal height/width $iPositionX = $aPosDashlet['position_x']; diff --git a/sources/Application/Dashlet/Core/DashletBadge.php b/sources/Application/Dashlet/Core/DashletBadge.php new file mode 100644 index 000000000..4fe782ab1 --- /dev/null +++ b/sources/Application/Dashlet/Core/DashletBadge.php @@ -0,0 +1,126 @@ + + +namespace Combodo\iTop\Application\Dashlet\Core; + +use Combodo\iTop\Application\Dashlet\Dashlet; +use Combodo\iTop\Application\UI\Base\Component\Dashlet\DashletContainer; +use DBObjectSearch; +use DesignerForm; +use DesignerIconSelectionField; +use Dict; +use DisplayBlock; +use utils; + +class DashletBadge extends Dashlet +{ + /** + * @inheritdoc + */ + public function __construct($oModelReflection, $sId) + { + parent::__construct($oModelReflection, $sId); + $this->aProperties['class'] = 'Contact'; + $this->aCSSClasses[] = 'ibo-dashlet--is-inline'; + $this->aCSSClasses[] = 'ibo-dashlet-badge'; + } + + /** + * @inheritdoc + * + * @throws \Exception + */ + public function Render($oPage, $bEditMode = false, $aExtraParams = []) + { + $oDashletContainer = new DashletContainer($this->sId, ['dashlet-content']); + + $sClass = $this->aProperties['class']; + $oFilter = new DBObjectSearch($sClass); + $oBlock = new DisplayBlock($oFilter, 'actions'); + $aExtraParams['context_filter'] = 1; + $aExtraParams['withJSRefreshCallBack'] = true; + $sBlockId = 'block_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occurring in the same DOM) + $oBlock->DisplayIntoContentBlock($oDashletContainer, $oPage, $sBlockId, $aExtraParams); + + return $oDashletContainer; + } + + /** + * @inheritdoc + */ + public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = []) + { + $oDashletContainer = new DashletContainer($this->sId, ['dashlet-content']); + + $sClass = $this->aProperties['class']; + $sIconUrl = utils::HtmlEntities($this->oModelReflection->GetClassIcon($sClass, false)); + $sClassLabel = $this->oModelReflection->GetName($sClass); + $sId = $this->sId; + $sClassCreate = Dict::Format('UI:ClickToCreateNew', $sClassLabel); + + $sHtml = << + + +HTML; + + $oDashletContainer->AddHtml($sHtml); + + return $oDashletContainer; + } + + protected static $aClassList = null; + + /** + * @inheritdoc + * + * @throws \Exception + */ + public function GetPropertiesFields(DesignerForm $oForm) + { + if (is_null(self::$aClassList)) { + // Cache the ordered list of classes (ordered on the label) + // (has a significant impact when editing a page with lots of badges) + // + $aClasses = []; + foreach ($this->oModelReflection->GetClasses('bizmodel', true /*exclude links*/) as $sClass) { + $aClasses[$sClass] = $this->oModelReflection->GetName($sClass); + } + asort($aClasses); + + self::$aClassList = []; + foreach ($aClasses as $sClass => $sLabel) { + $sIconUrl = $this->oModelReflection->GetClassIcon($sClass, false); + if ($sIconUrl == '') { + // The icon does not exist, let's use a transparent one of the same size. + $sIconUrl = utils::GetAbsoluteUrlAppRoot().'images/transparent_32_32.png'; + } + self::$aClassList[] = ['value' => $sClass, 'label' => $sLabel, 'icon' => $sIconUrl]; + } + } + + $oField = new DesignerIconSelectionField('class', Dict::S('UI:DashletBadge:Prop-Class'), $this->aProperties['class']); + $oField->SetAllowedValues(self::$aClassList); + + $oForm->AddField($oField); + } +} diff --git a/sources/Application/Dashlet/Core/DashletGroupBy.php b/sources/Application/Dashlet/Core/DashletGroupBy.php new file mode 100644 index 000000000..3d4f676ba --- /dev/null +++ b/sources/Application/Dashlet/Core/DashletGroupBy.php @@ -0,0 +1,590 @@ + + +namespace Combodo\iTop\Application\Dashlet\Core; + +use Combodo\iTop\Application\Dashlet\Dashlet; +use Combodo\iTop\Application\UI\Base\Component\Dashlet\DashletContainer; +use Combodo\iTop\Application\UI\Base\Component\Html\Html; +use DBObjectSearch; +use DesignerComboField; +use DesignerForm; +use DesignerFormSelectorField; +use DesignerHiddenField; +use DesignerIntegerField; +use DesignerLongTextField; +use DesignerTextField; +use Dict; +use DisplayBlock; +use Exception; +use MetaModel; +use utils; + +abstract class DashletGroupBy extends Dashlet +{ + public function __construct($oModelReflection, $sId, string $sDashletType = null) + { + parent::__construct($oModelReflection, $sId, $sDashletType); + $this->aProperties['title'] = ''; + $this->aProperties['query'] = 'SELECT Contact'; + $this->aProperties['group_by'] = 'status'; + $this->aProperties['style'] = 'table'; + $this->aProperties['aggregation_function'] = 'count'; + $this->aProperties['aggregation_attribute'] = ''; + $this->aProperties['limit'] = ''; + $this->aProperties['order_by'] = ''; + $this->aProperties['order_direction'] = ''; + } + + protected $sGroupByLabel = null; + protected $sGroupByExpr = null; + protected $sGroupByAttCode = null; + protected $sFunction = null; + protected $sAggregationFunction = null; + protected $sAggregationAttribute = null; + protected $sLimit = null; + protected $sOrderBy = null; + protected $sOrderDirection = null; + protected $sClass = null; + + /** + * Compute Grouping + * + * @inheritdoc + */ + public function OnUpdate() + { + $this->sGroupByExpr = null; + $this->sGroupByLabel = null; + $this->sGroupByAttCode = null; + $this->sFunction = null; + $this->sClass = null; + + $sQuery = $this->aProperties['query']; + $sGroupBy = $this->aProperties['group_by']; + + $this->sAggregationFunction = $this->aProperties['aggregation_function']; + $this->sAggregationAttribute = $this->aProperties['aggregation_attribute'] ?? ''; + + $this->sLimit = $this->aProperties['limit'] ?? 0; + $this->sOrderBy = $this->aProperties['order_by'] ?? null; + if (empty($this->sOrderBy)) { + if ($this->aProperties['style'] == 'pie') { + $this->sOrderBy = 'function'; + } else { + $this->sOrderBy = 'attribute'; + } + } + + // First perform the query - if the OQL is not ok, it will generate an exception : no need to go further + try { + $oQuery = $this->oModelReflection->GetQuery($sQuery); + $this->sClass = $oQuery->GetClass(); + $sClassAlias = $oQuery->GetClassAlias(); + } catch (Exception $e) { + // Invalid query, let the user edit the dashlet/dashboard anyhow + $this->sClass = null; + $sClassAlias = ''; + } + + // Check groupby... it can be wrong at this stage + if (preg_match('/^(.*):(.*)$/', $sGroupBy, $aMatches)) { + $this->sGroupByAttCode = $aMatches[1]; + $this->sFunction = $aMatches[2]; + } else { + $this->sGroupByAttCode = $sGroupBy; + $this->sFunction = null; + } + + if ((!is_null($this->sClass)) && empty($this->aProperties['order_direction'])) { + $aAttributeTypes = $this->oModelReflection->ListAttributes($this->sClass); + if (isset($aAttributeTypes[$this->sGroupByAttCode])) { + $sAttributeType = $aAttributeTypes[$this->sGroupByAttCode]; + if (is_subclass_of($sAttributeType, 'AttributeDateTime') || $sAttributeType == 'AttributeDateTime') { + $this->sOrderDirection = 'asc'; + } else { + $this->sOrderDirection = 'desc'; + } + } + } else { + $this->sOrderDirection = $this->aProperties['order_direction']; + } + + if ((!is_null($this->sClass)) && $this->oModelReflection->IsValidAttCode($this->sClass, $this->sGroupByAttCode)) { + $sAttLabel = $this->oModelReflection->GetLabel($this->sClass, $this->sGroupByAttCode); + if (!is_null($this->sFunction)) { + switch ($this->sFunction) { + case 'hour': + $this->sGroupByLabel = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Hour', $sAttLabel); + $this->sGroupByExpr = "DATE_FORMAT($sClassAlias.{$this->sGroupByAttCode}, '%H')"; // 0 -> 23 + break; + + case 'month': + $this->sGroupByLabel = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Month', $sAttLabel); + $this->sGroupByExpr = "DATE_FORMAT($sClassAlias.{$this->sGroupByAttCode}, '%Y-%m')"; // yyyy-mm + break; + + case 'day_of_week': + $this->sGroupByLabel = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:DayOfWeek', $sAttLabel); + $this->sGroupByExpr = "DATE_FORMAT($sClassAlias.{$this->sGroupByAttCode}, '%w')"; + break; + + case 'day_of_month': + $this->sGroupByLabel = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:DayOfMonth', $sAttLabel); + $this->sGroupByExpr = "DATE_FORMAT($sClassAlias.{$this->sGroupByAttCode}, '%Y-%m-%d')"; // mm-dd + break; + + default: + $this->sGroupByLabel = 'Unknown group by function '.$this->sFunction; + $this->sGroupByExpr = $sClassAlias.'.'.$this->sGroupByAttCode; + } + } else { + $this->sGroupByExpr = $sClassAlias.'.'.$this->sGroupByAttCode; + $this->sGroupByLabel = $sAttLabel; + } + } else { + $this->sGroupByAttCode = null; + } + } + + /** + * @inheritdoc + * + * @throws \CoreException + * @throws \ArchivedObjectException + */ + public function Render($oPage, $bEditMode = false, $aExtraParams = []) + { + $sTitle = $this->aProperties['title']; + $sQuery = $this->aProperties['query']; + $sStyle = $this->aProperties['style']; + + // First perform the query - if the OQL is not ok, it will generate an exception : no need to go further + if (isset($aExtraParams['query_params'])) { + $aQueryParams = $aExtraParams['query_params']; + } elseif (isset($aExtraParams['this->class']) && isset($aExtraParams['this->id'])) { + $oObj = MetaModel::GetObject($aExtraParams['this->class'], $aExtraParams['this->id']); + $aQueryParams = $oObj->ToArgsForQuery(); + } else { + $aQueryParams = []; + } + $oFilter = DBObjectSearch::FromOQL($sQuery, $aQueryParams); + $oFilter->SetShowObsoleteData(utils::ShowObsoleteData()); + + $sClass = $oFilter->GetClass(); + if (!$this->oModelReflection->IsValidAttCode($sClass, $this->sGroupByAttCode)) { + return new Html('

'.Dict::S('UI:DashletGroupBy:MissingGroupBy').'

'); + } + + switch ($sStyle) { + case 'bars': + $sType = 'chart'; + $aParams = [ + 'chart_type' => 'bars', + 'chart_title' => $sTitle, + 'group_by' => $this->sGroupByExpr, + 'group_by_label' => $this->sGroupByLabel, + 'aggregation_function' => $this->sAggregationFunction, + 'aggregation_attribute' => $this->sAggregationAttribute, + 'limit' => $this->sLimit, + 'order_direction' => $this->sOrderDirection, + 'order_by' => $this->sOrderBy, + ]; + $sHtmlTitle = ''; // done in the itop block + break; + + case 'pie': + $sType = 'chart'; + $aParams = [ + 'chart_type' => 'pie', + 'chart_title' => $sTitle, + 'group_by' => $this->sGroupByExpr, + 'group_by_label' => $this->sGroupByLabel, + 'aggregation_function' => $this->sAggregationFunction, + 'aggregation_attribute' => $this->sAggregationAttribute, + 'limit' => $this->sLimit, + 'order_direction' => $this->sOrderDirection, + 'order_by' => $this->sOrderBy, + ]; + $sHtmlTitle = ''; // done in the itop block + break; + + case 'table': + default: + $sHtmlTitle = utils::HtmlEntities(Dict::S($sTitle)); // done in the itop block + $sType = 'count'; + $aParams = [ + 'group_by' => $this->sGroupByExpr, + 'group_by_label' => $this->sGroupByLabel, + 'aggregation_function' => $this->sAggregationFunction, + 'aggregation_attribute' => $this->sAggregationAttribute, + 'limit' => $this->sLimit, + 'order_direction' => $this->sOrderDirection, + 'order_by' => $this->sOrderBy, + ]; + break; + } + + //$oPanel = \Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory::MakeStandard(); + //PanelUIBlockFactory::MakeForClass($sClass, Dict::S($sTitle)); + + $sBlockId = 'block_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occurring in the same DOM) + $oBlock = new DisplayBlock($oFilter, $sType); + //$oBlock->DisplayIntoContentBlock($oPanel, $oPage, $sBlockId, array_merge($aExtraParams, $aParams)); + $aExtraParams["surround_with_panel"] = true; + $aExtraParams["panel_title"] = Dict::S($sTitle); + $aExtraParams["panel_class"] = $sClass; + $oPanel = $oBlock->GetDisplay($oPage, $sBlockId, array_merge($aExtraParams, $aParams)); + if ($bEditMode) { + $oPanel->AddHtml('
'); + } + + return $oPanel; + } + + /** + * @return array + */ + protected function MakeSimulatedData() + { + $sQuery = $this->aProperties['query']; + + $oQuery = $this->oModelReflection->GetQuery($sQuery); + $sClass = $oQuery->GetClass(); + + $aDisplayValues = []; + if ($this->oModelReflection->IsValidAttCode($sClass, $this->sGroupByAttCode)) { + $aAttributeTypes = $this->oModelReflection->ListAttributes($sClass); + $sAttributeType = $aAttributeTypes[$this->sGroupByAttCode]; + if (is_subclass_of($sAttributeType, 'AttributeDateTime') || $sAttributeType == 'AttributeDateTime') { + // Note: an alternative to this somewhat hardcoded way of doing things would be to implement... + //$oExpr = Expression::FromOQL($this->sGroupByExpr); + //$aTranslationData = array($oQuery->GetClassAlias() => array($this->sGroupByAttCode => new ScalarExpression(date('Y-m-d H:i:s', $iTime)))); + //$sRawValue = CMDBSource::QueryToScalar('SELECT '.$oExpr->Translate($aTranslationData)->Render()); + //$sValueLabel = $oExpr->MakeValueLabel(oFilter, $sRawValue, $sRawValue); + // Anyhow, this requires : + // - an update to the prototype of MakeValueLabel() so that it takes ModelReflection parameters + // - propose clever date/times samples + + $aValues = []; + switch ($this->sFunction) { + case 'hour': + $aValues = [8, 9, 15, 18]; + break; + + case 'month': + $aValues = ['2013 '.Dict::S('Month-11'), '2013 '.Dict::S('Month-12'), '2014 '.Dict::S('Month-01'), '2014 '.Dict::S('Month-02'), '2014 '.Dict::S('Month-03')]; + break; + + case 'day_of_week': + $aValues = [Dict::S('DayOfWeek-Monday'), Dict::S('DayOfWeek-Wednesday'), Dict::S('DayOfWeek-Thursday'), Dict::S('DayOfWeek-Friday')]; + break; + + case 'day_of_month': + $aValues = [Dict::S('Month-03').' 30', Dict::S('Month-03').' 31', Dict::S('Month-04').' 01', Dict::S('Month-04').' 02', Dict::S('Month-04').' 03']; + break; + } + foreach ($aValues as $sValue) { + $aDisplayValues[] = ['label' => $sValue, 'value' => (int)rand(1, 15)]; + } + } elseif (is_subclass_of($sAttributeType, 'AttributeEnum') || $sAttributeType == 'AttributeEnum') { + $aAllowed = $this->oModelReflection->GetAllowedValues_att($sClass, $this->sGroupByAttCode); + if ($aAllowed) { // null for non enums + foreach ($aAllowed as $sValue => $sValueLabel) { + $iCount = (int)rand(2, 100); + $aDisplayValues[] = [ + 'label' => $sValueLabel, + 'value' => $iCount, + ]; + } + } + } else { + $aDisplayValues[] = ['label' => 'a', 'value' => 123]; + $aDisplayValues[] = ['label' => 'b', 'value' => 321]; + $aDisplayValues[] = ['label' => 'c', 'value' => 456]; + } + } + + return $aDisplayValues; + } + + /** + * @inheritdoc + */ + public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = []) + { + $oDashletContainer = new DashletContainer(null, ['dashlet-content']); + $oDashletContainer->AddHtml('error!'); + + return $oDashletContainer; + } + + /** + * @inheritdoc + */ + public function GetPropertiesFields(DesignerForm $oForm) + { + $oField = new DesignerTextField('title', Dict::S('UI:DashletGroupBy:Prop-Title'), $this->aProperties['title']); + $oForm->AddField($oField); + + $oField = new DesignerLongTextField('query', Dict::S('UI:DashletGroupBy:Prop-Query'), $this->aProperties['query']); + $oField->SetMandatory(); + $oField->AddCSSClass("ibo-query-oql"); + $oField->AddCSSClass("ibo-is-code"); + $oForm->AddField($oField); + + try { + // Group by field: build the list of possible values (attribute codes + ...) + $aGroupBy = $this->GetGroupByOptions($this->aProperties['query']); + + $oField = new DesignerComboField('group_by', Dict::S('UI:DashletGroupBy:Prop-GroupBy'), $this->aProperties['group_by']); + $oField->SetMandatory(); + $oField->SetAllowedValues($aGroupBy); + } catch (Exception $e) { + $oField = new DesignerTextField('group_by', Dict::S('UI:DashletGroupBy:Prop-GroupBy'), $this->aProperties['group_by']); + $oField->SetReadOnly(); + $aGroupBy = []; + } + $oForm->AddField($oField); + + $aStyles = [ + 'pie' => Dict::S('UI:DashletGroupByPie:Label'), + 'bars' => Dict::S('UI:DashletGroupByBars:Label'), + 'table' => Dict::S('UI:DashletGroupByTable:Label'), + ]; + + $oField = new DesignerComboField('style', Dict::S('UI:DashletGroupBy:Prop-Style'), $this->aProperties['style']); + $oField->SetMandatory(); + $oField->SetAllowedValues($aStyles); + $oForm->AddField($oField); + + $aFunctionAttributes = $this->GetNumericAttributes($this->aProperties['query']); + $aFunctions = $this->GetAllowedFunctions($aFunctionAttributes); + $oSelectorField = new DesignerFormSelectorField('aggregation_function', Dict::S('UI:DashletGroupBy:Prop-Function'), $this->aProperties['aggregation_function']); + $oForm->AddField($oSelectorField); + $oSelectorField->SetMandatory(); + // Count sub-menu + $oSubForm = new DesignerForm(); + $oSelectorField->AddSubForm($oSubForm, Dict::S('UI:GroupBy:count'), 'count'); + foreach ($aFunctions as $sFct => $sLabel) { + $oSubForm = new DesignerForm(); + $oField = new DesignerComboField('aggregation_attribute', Dict::S('UI:DashletGroupBy:Prop-FunctionAttribute'), $this->aProperties['aggregation_attribute']); + $oField->SetMandatory(); + $oField->SetAllowedValues($aFunctionAttributes); + $oSubForm->AddField($oField); + $oSelectorField->AddSubForm($oSubForm, $sLabel, $sFct); + } + + $aOrderField = []; + + if (isset($this->aProperties['group_by']) && isset($aGroupBy[$this->aProperties['group_by']])) { + $aOrderField['attribute'] = $aGroupBy[$this->aProperties['group_by']]; + } + + if ($this->aProperties['aggregation_function'] == 'count') { + $aOrderField['function'] = Dict::S('UI:GroupBy:count'); + } else { + $aOrderField['function'] = $aFunctions[$this->aProperties['aggregation_function']]; + } + $oSelectorField = new DesignerFormSelectorField('order_by', Dict::S('UI:DashletGroupBy:Prop-OrderField'), $this->aProperties['order_by']); + $oForm->AddField($oSelectorField); + $oSelectorField->SetMandatory(); + foreach ($aOrderField as $sField => $sLabel) { + $oSubForm = new DesignerForm(); + if ($sField == 'function') { + $oField = new DesignerIntegerField('limit', Dict::S('UI:DashletGroupBy:Prop-Limit'), $this->aProperties['limit']); + $oSubForm->AddField($oField); + } + $oSelectorField->AddSubForm($oSubForm, $sLabel, $sField); + } + + $aOrderDirections = [ + 'asc' => Dict::S('UI:DashletGroupBy:Order:asc'), + 'desc' => Dict::S('UI:DashletGroupBy:Order:desc'), + ]; + $sOrderDirection = empty($this->aProperties['order_direction']) ? $this->sOrderDirection : $this->aProperties['order_direction']; + $oField = new DesignerComboField('order_direction', Dict::S('UI:DashletGroupBy:Prop-OrderDirection'), $sOrderDirection); + $oField->SetMandatory(); + $oField->SetAllowedValues($aOrderDirections); + $oForm->AddField($oField); + + } + + /** + * @return array + */ + protected function GetOrderBy() + { + if (is_null($this->sClass)) { + return []; + } + + return [ + $this->aProperties['group_by'] => $this->oModelReflection->GetLabel($this->sClass, $this->aProperties['group_by']), + '_itop_'.$this->aProperties['aggregation_function'].'_' => Dict::S('UI:GroupBy:'.$this->aProperties['aggregation_function']), + ]; + } + + /** + * @param array $aFunctionAttributes + * + * @return array + */ + protected function GetAllowedFunctions($aFunctionAttributes) + { + $aFunctions = []; + + if (!empty($aFunctionAttributes) || is_null($this->sClass)) { + $aFunctions['sum'] = Dict::S('UI:GroupBy:sum'); + $aFunctions['avg'] = Dict::S('UI:GroupBy:avg'); + $aFunctions['min'] = Dict::S('UI:GroupBy:min'); + $aFunctions['max'] = Dict::S('UI:GroupBy:max'); + } + + return $aFunctions; + } + + /** + * @param string $sOql + * + * @return array + */ + protected function GetNumericAttributes($sOql) + { + $aFunctionAttributes = []; + try { + $oQuery = $this->oModelReflection->GetQuery($sOql); + $sClass = $oQuery->GetClass(); + if (is_null($sClass)) { + return $aFunctionAttributes; + } + foreach ($this->oModelReflection->ListAttributes($sClass) as $sAttCode => $sAttType) { + switch ($sAttType) { + case 'AttributeDecimal': + case 'AttributeDuration': + case 'AttributeInteger': + case 'AttributePercentage': + case 'AttributeSubItem': // TODO: Known limitation: no unit displayed (values in sec) + $sLabel = $this->oModelReflection->GetLabel($sClass, $sAttCode); + $aFunctionAttributes[$sAttCode] = $sLabel; + break; + } + } + } catch (Exception $e) { + // In case the OQL is bad + } + + return $aFunctionAttributes; + } + + /** + * @inheritdoc + */ + public function Update($aValues, $aUpdatedFields) + { + if (in_array('query', $aUpdatedFields)) { + try { + $sCurrQuery = $aValues['query']; + $oCurrSearch = $this->oModelReflection->GetQuery($sCurrQuery); + $sCurrClass = $oCurrSearch->GetClass(); + + $sPrevQuery = $this->aProperties['query']; + $oPrevSearch = $this->oModelReflection->GetQuery($sPrevQuery); + $sPrevClass = $oPrevSearch->GetClass(); + + if ($sCurrClass != $sPrevClass) { + $this->bFormRedrawNeeded = true; + // wrong but not necessary - unset($aUpdatedFields['group_by']); + $this->aProperties['group_by'] = ''; + } + } catch (Exception $e) { + $this->bFormRedrawNeeded = true; + } + } + $oDashlet = parent::Update($aValues, $aUpdatedFields); + + if (in_array('style', $aUpdatedFields)) { + switch ($aValues['style']) { + // Style changed, mutate to the specified type of chart + case 'pie': + $oDashlet = new DashletGroupByPie($this->oModelReflection, $this->sId); + break; + + case 'bars': + $oDashlet = new DashletGroupByBars($this->oModelReflection, $this->sId); + break; + + case 'table': + $oDashlet = new DashletGroupByTable($this->oModelReflection, $this->sId); + break; + } + $oDashlet->FromParams($aValues); + $oDashlet->bRedrawNeeded = true; + $oDashlet->bFormRedrawNeeded = true; + } + if (in_array('aggregation_attribute', $aUpdatedFields) || in_array('order_direction', $aUpdatedFields) || in_array('order_by', $aUpdatedFields) || in_array('limit', $aUpdatedFields)) { + $oDashlet->bRedrawNeeded = true; + } + if (in_array('group_by', $aUpdatedFields) || in_array('aggregation_function', $aUpdatedFields)) { + $oDashlet->bRedrawNeeded = true; + $oDashlet->bFormRedrawNeeded = true; + } + + return $oDashlet; + } + + /** + * @inheritdoc + */ + public static function CanCreateFromOQL() + { + return true; + } + + /** + * @inheritdoc + */ + public function GetPropertiesFieldsFromOQL(DesignerForm $oForm, $sOQL = null) + { + $oField = new DesignerTextField('title', Dict::S('UI:DashletGroupBy:Prop-Title'), ''); + $oForm->AddField($oField); + + $oField = new DesignerHiddenField('query', Dict::S('UI:DashletGroupBy:Prop-Query'), $sOQL); + $oField->SetMandatory(); + $oField->AddCSSClass("ibo-query-oql"); + $oField->AddCSSClass("ibo-is-code"); + $oForm->AddField($oField); + + if (!is_null($sOQL)) { + $oField = new DesignerComboField('group_by', Dict::S('UI:DashletGroupBy:Prop-GroupBy'), null); + $aGroupBy = $this->GetGroupByOptions($sOQL); + $oField->SetAllowedValues($aGroupBy); + } else { + // Creating a form for reading parameters! + $oField = new DesignerTextField('group_by', Dict::S('UI:DashletGroupBy:Prop-GroupBy'), null); + } + $oField->SetMandatory(); + + $oForm->AddField($oField); + + $oField = new DesignerHiddenField('style', '', $this->aProperties['style']); + $oField->SetMandatory(); + $oForm->AddField($oField); + } +} diff --git a/sources/Application/Dashlet/Core/DashletGroupByBars.php b/sources/Application/Dashlet/Core/DashletGroupByBars.php new file mode 100644 index 000000000..4c564efad --- /dev/null +++ b/sources/Application/Dashlet/Core/DashletGroupByBars.php @@ -0,0 +1,112 @@ + + +namespace Combodo\iTop\Application\Dashlet\Core; + +use Combodo\iTop\Application\UI\Base\Component\Dashlet\DashletContainer; +use Dict; +use utils; + +class DashletGroupByBars extends DashletGroupBy +{ + /** + * @inheritdoc + */ + public function __construct($oModelReflection, $sId) + { + parent::__construct($oModelReflection, $sId); + $this->aProperties['style'] = 'bars'; + } + + /** + * @inheritdoc + */ + public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = []) + { + $oDashletContainer = new DashletContainer(null, ['dashlet-content']); + + $sTitle = $this->aProperties['title']; + + $sBlockId = 'block_fake_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occuring in the same DOM) + + $HTMLsTitle = ($sTitle != '') ? '

'.utils::HtmlEntities($sTitle).'

' : ''; + $oDashletContainer->AddHtml("
$HTMLsTitle
"); + + $aDisplayValues = $this->MakeSimulatedData(); + + $aNames = []; + foreach ($aDisplayValues as $idx => $aValue) { + $aNames[$idx] = $aValue['label']; + } + $sJSNames = json_encode($aNames); + + $sJson = json_encode($aDisplayValues); + $oPage->add_ready_script( + << + +namespace Combodo\iTop\Application\Dashlet\Core; + +use Combodo\iTop\Application\Helper\WebResourcesHelper; +use Combodo\iTop\Application\UI\Base\Component\Dashlet\DashletContainer; +use Dict; +use utils; + +class DashletGroupByPie extends DashletGroupBy +{ + /** + * @inheritdoc + */ + public function __construct($oModelReflection, $sId) + { + parent::__construct($oModelReflection, $sId); + $this->aProperties['style'] = 'pie'; + } + + /** + * @inheritDoc + */ + public function GetJSFilesRelPaths(): array + { + return array_merge( + parent::GetJSFilesRelPaths(), + WebResourcesHelper::GetJSFilesRelPathsForC3JS() + ); + } + + /** + * @inheritDoc + */ + public function GetCSSFilesRelPaths(): array + { + return array_merge( + parent::GetCSSFilesRelPaths(), + WebResourcesHelper::GetCSSFilesRelPathsForC3JS() + ); + } + + /** + * @inheritdoc + */ + public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = []) + { + $oDashletContainer = new DashletContainer(null, ['dashlet-content']); + + $sTitle = $this->aProperties['title']; + + $sBlockId = 'block_fake_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occuring in the same DOM) + + $HTMLsTitle = ($sTitle != '') ? '

'.utils::HtmlEntities($sTitle).'

' : ''; + $oDashletContainer->AddHtml("
$HTMLsTitle
"); + + $aDisplayValues = $this->MakeSimulatedData(); + + $aColumns = []; + $aNames = []; + foreach ($aDisplayValues as $idx => $aValue) { + $aColumns[] = ['series_'.$idx, (int)$aValue['value']]; + $aNames['series_'.$idx] = $aValue['label']; + } + $sJSColumns = json_encode($aColumns); + $sJSNames = json_encode($aNames); + $oPage->add_ready_script( + << + +namespace Combodo\iTop\Application\Dashlet\Core; + +use Combodo\iTop\Application\UI\Base\Component\Dashlet\DashletContainer; +use Dict; + +class DashletGroupByTable extends DashletGroupBy +{ + /** + * @inheritdoc + */ + public function __construct($oModelReflection, $sId) + { + parent::__construct($oModelReflection, $sId); + $this->aProperties['style'] = 'table'; + } + + /** + * @inheritdoc + */ + public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = []) + { + $oDashletContainer = new DashletContainer(); + + $aDisplayValues = $this->MakeSimulatedData(); + $iTotal = 0; + foreach ($aDisplayValues as $iRow => $aDisplayData) { + $iTotal += $aDisplayData['value']; + } + + $sBlockId = 'block_fake_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occuring in the same DOM) + + $sHtml = ''; + $sHtml .= '
'; + $sHtml .= '
'; + $sHtml .= '

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

'; + $sHtml .= ''; + $sHtml .= ''; + $sHtml .= ''; + $sHtml .= ''; + $sHtml .= ''; + $sHtml .= ''; + $sHtml .= ''; + $sHtml .= ''; + foreach ($aDisplayValues as $aDisplayData) { + $sHtml .= ''; + $sHtml .= ''; + $sHtml .= ''; + $sHtml .= ''; + } + $sHtml .= ''; + $sHtml .= '
'.$this->sGroupByLabel.''.Dict::S('UI:GroupBy:Count').'
'.$aDisplayData['label'].''.$aDisplayData['value'].'
'; + $sHtml .= '
'; + + $sHtml .= '
'; + + $oDashletContainer->AddHtml($sHtml); + + return $oDashletContainer; + } +} diff --git a/sources/Application/Dashlet/Core/DashletHeaderDynamic.php b/sources/Application/Dashlet/Core/DashletHeaderDynamic.php new file mode 100644 index 000000000..94dc71c70 --- /dev/null +++ b/sources/Application/Dashlet/Core/DashletHeaderDynamic.php @@ -0,0 +1,326 @@ + + +namespace Combodo\iTop\Application\Dashlet\Core; + +use ApplicationContext; +use Combodo\iTop\Application\Dashlet\Dashlet; +use Combodo\iTop\Application\UI\Base\Component\Dashlet\DashletContainer; +use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory; +use DBObjectSearch; +use DBObjectSet; +use DesignerComboField; +use DesignerForm; +use DesignerLongTextField; +use DesignerTextField; +use Dict; +use DisplayBlock; +use Exception; +use MetaModel; +use UnknownClassOqlException; +use utils; + +class DashletHeaderDynamic extends Dashlet +{ + /** + * @inheritdoc + */ + public function __construct($oModelReflection, $sId) + { + parent::__construct($oModelReflection, $sId); + $this->aProperties['title'] = Dict::S('UI:DashletHeaderDynamic:Prop-Title:Default'); + $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'] = ['active', 'inactive']; + } + + /** + * @return array + */ + protected function GetValues() + { + $sQuery = $this->aProperties['query']; + $sGroupBy = $this->aProperties['group_by']; + $aValues = $this->aProperties['values']; + + if (empty($aValues)) { + $aValues = []; + } + + $oQuery = $this->oModelReflection->GetQuery($sQuery); + $sClass = $oQuery->GetClass(); + + if ($this->oModelReflection->IsValidAttCode($sClass, $sGroupBy)) { + if (count($aValues) == 0) { + $aAllowed = $this->oModelReflection->GetAllowedValues_att($sClass, $sGroupBy); + if (is_array($aAllowed)) { + $aValues = array_keys($aAllowed); + } + } + } + + return $aValues; + } + + /** + * @inheritdoc + * + * @throws \CoreException + * @throws \ArchivedObjectException + */ + public function Render($oPage, $bEditMode = false, $aExtraParams = []) + { + $sTitle = utils::HtmlEntities($this->aProperties['title']); + $sIcon = $this->aProperties['icon']; + $sSubtitle = utils::HtmlEntities($this->aProperties['subtitle']); + $sQuery = $this->aProperties['query']; + $sGroupBy = $this->aProperties['group_by']; + + $oIconSelect = $this->oModelReflection->GetIconSelectionField('icon'); + $sIconPath = ''; + if (Utils::IsNotNullOrEmptyString($sIcon)) { + $sIconPath = $oIconSelect->MakeFileUrl($sIcon); + } + + $aValues = $this->GetValues(); + if (count($aValues) > 0) { + // Stats grouped by + $sCSV = implode(',', $aValues); + $aParams = [ + 'title[block]' => $sTitle, + 'label[block]' => $sSubtitle, + 'status[block]' => $sGroupBy, + 'status_codes[block]' => $sCSV, + 'context_filter' => 1, + ]; + } else { + // Simple stats + $aParams = [ + 'title[block]' => $sTitle, + 'label[block]' => $sSubtitle, + 'context_filter' => 1, + ]; + } + + if (isset($aExtraParams['query_params'])) { + $aQueryParams = $aExtraParams['query_params']; + } elseif (isset($aExtraParams['this->class'])) { + $oObj = MetaModel::GetObject($aExtraParams['this->class'], $aExtraParams['this->id']); + $aQueryParams = $oObj->ToArgsForQuery(); + } else { + $aQueryParams = []; + } + $oFilter = DBObjectSearch::FromOQL($sQuery, $aQueryParams); + $oFilter->SetShowObsoleteData(utils::ShowObsoleteData()); + $sClass = $oFilter->GetClass(); + + $oPanel = PanelUIBlockFactory::MakeNeutral(Dict::S(str_replace('_', ':', $sTitle))) + ->SetIcon($sIconPath) + ->SetColorFromClass($sClass); + $oBlock = new DisplayBlock($oFilter, 'summary'); + $sBlockId = 'block_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occuring in the same DOM) + $oBlock->DisplayIntoContentBlock($oPanel, $oPage, $sBlockId, array_merge($aExtraParams, $aParams)); + + $oSubTitle = $oPanel->GetSubTitleBlock(); + $oSet = new DBObjectSet($oFilter); + $iCount = $oSet->Count(); + $oAppContext = new ApplicationContext(); + $sHyperlink = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=search'.$oAppContext->GetForLink(true).'&filter='.rawurlencode($oFilter->serialize()); + $oSubTitle->AddHtml(''.Dict::Format(str_replace('_', ':', $sSubtitle), $iCount).''); + + return $oPanel; + } + + /** + * @inheritdoc + */ + public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = []) + { + $sTitle = utils::HtmlEntities($this->aProperties['title']); + $sIcon = $this->aProperties['icon']; + $sSubtitle = utils::HtmlEntities($this->aProperties['subtitle']); + $sQuery = $this->aProperties['query']; + $sGroupBy = $this->aProperties['group_by']; + + $aValueLabels = []; + $aValues = []; + try { + $oQuery = $this->oModelReflection->GetQuery($sQuery); + $sClass = $oQuery->GetClass(); + $aValues = $this->GetValues(); + foreach ($aValues as $sValue) { + $aValueLabels[] = $this->oModelReflection->GetValueLabel($sClass, $sGroupBy, $sValue); + } + } catch (UnknownClassOqlException $e) { + $aValueLabels[] = $e->GetUserFriendlyDescription(); + $aValues[] = 1; + } + + $oIconSelect = $this->oModelReflection->GetIconSelectionField('icon'); + $sIconPath = utils::HtmlEntities($oIconSelect->MakeFileUrl($sIcon)); + + $oDashletContainer = new DashletContainer(null, ['dashlet-content']); + + $sHtml = ''; + $sHtml .= ''; + + $sBlockId = 'block_fake_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occuring in the same DOM) + + $iTotal = 0; + + $sHtml .= '
'; + $sHtml .= '
'; + $sHtml .= ''; + $sHtml .= ''; + foreach ($aValueLabels as $sValueLabel) { + $sHtml .= ' '; + } + $sHtml .= ''; + $sHtml .= ''; + foreach ($aValues as $sValue) { + $iCount = rand(2, 100); + $iTotal += $iCount; + $sHtml .= ' '; + } + $sHtml .= ''; + $sHtml .= '
'.$sValueLabel.'
'.$iCount.'
'; + $sHtml .= '
'; + + $sTitle = $this->oModelReflection->DictString($sTitle); + $sSubtitle = $this->oModelReflection->DictFormat($sSubtitle, $iTotal); + + $sHtml .= '

'.utils::HtmlEntities($sTitle).'

'; + $sHtml .= ''.utils::HtmlEntities($sSubtitle).''; + $sHtml .= '
'; + + $oDashletContainer->AddHtml($sHtml); + + return $oDashletContainer; + + } + + /** + * @inheritdoc + */ + public function GetPropertiesFields(DesignerForm $oForm) + { + $oField = new DesignerTextField('title', Dict::S('UI:DashletHeaderDynamic:Prop-Title'), $this->aProperties['title']); + $oForm->AddField($oField); + + $oField = $this->oModelReflection->GetIconSelectionField('icon', Dict::S('UI:DashletHeaderDynamic:Prop-Icon'), $this->aProperties['icon']); + $oField->AddAllowedValue(['value' => '', 'label' => Dict::S('UI:DashletIcon:None'), 'icon' => '']); + $oForm->AddField($oField); + + $oField = new DesignerTextField('subtitle', Dict::S('UI:DashletHeaderDynamic:Prop-Subtitle'), $this->aProperties['subtitle']); + $oForm->AddField($oField); + + $oField = new DesignerLongTextField('query', Dict::S('UI:DashletHeaderDynamic:Prop-Query'), $this->aProperties['query']); + $oField->SetMandatory(); + $oField->AddCSSClass("ibo-query-oql"); + $oField->AddCSSClass("ibo-is-code"); + $oForm->AddField($oField); + + try { + // Group by field: build the list of possible values (attribute codes + ...) + $oQuery = $this->oModelReflection->GetQuery($this->aProperties['query']); + $sClass = $oQuery->GetClass(); + $aGroupBy = $this->GetGroupByOptions($this->aProperties['query']); + $oField = new DesignerComboField('group_by', Dict::S('UI:DashletHeaderDynamic:Prop-GroupBy'), $this->aProperties['group_by']); + $oField->SetMandatory(); + $oField->SetAllowedValues($aGroupBy); + } catch (Exception $e) { + $oField = new DesignerTextField('group_by', Dict::S('UI:DashletHeaderDynamic:Prop-GroupBy'), $this->aProperties['group_by']); + $oField->SetReadOnly(); + } + $oForm->AddField($oField); + + $oField = new DesignerComboField('values', Dict::S('UI:DashletHeaderDynamic:Prop-Values'), $this->aProperties['values']); + $oField->MultipleSelection(true); + if (isset($sClass) && $this->oModelReflection->IsValidAttCode($sClass, $this->aProperties['group_by'])) { + $aValues = $this->oModelReflection->GetAllowedValues_att($sClass, $this->aProperties['group_by']); + $oField->SetAllowedValues($aValues); + } else { + $oField->SetReadOnly(); + } + $oForm->AddField($oField); + } + + /** + * @inheritdoc + */ + public function Update($aValues, $aUpdatedFields) + { + if (in_array('query', $aUpdatedFields)) { + try { + $sCurrQuery = $aValues['query']; + $oCurrSearch = $this->oModelReflection->GetQuery($sCurrQuery); + $sCurrClass = $oCurrSearch->GetClass(); + + $sPrevQuery = $this->aProperties['query']; + $oPrevSearch = $this->oModelReflection->GetQuery($sPrevQuery); + $sPrevClass = $oPrevSearch->GetClass(); + + if ($sCurrClass != $sPrevClass) { + $this->bFormRedrawNeeded = true; + // wrong but not necessary - unset($aUpdatedFields['group_by']); + $this->aProperties['group_by'] = ''; + $this->aProperties['values'] = []; + } + } catch (Exception $e) { + $this->bFormRedrawNeeded = true; + } + } + if (in_array('group_by', $aUpdatedFields)) { + $this->bFormRedrawNeeded = true; + $this->aProperties['values'] = []; + } + + return parent::Update($aValues, $aUpdatedFields); + } + + /** + * @inheritdoc + */ + protected function PropertyFromDOMNode($oDOMNode, $sProperty) + { + if ($sProperty == 'icon') { + $oIconField = $this->oModelReflection->GetIconSelectionField('icon'); + + return $oIconField->ValueFromDOMNode($oDOMNode); + } else { + return parent::PropertyFromDOMNode($oDOMNode, $sProperty); + } + } + + /** + * @inheritdoc + */ + protected function PropertyToDOMNode($oDOMNode, $sProperty, $value) + { + if ($sProperty == 'icon') { + $oIconField = $this->oModelReflection->GetIconSelectionField('icon'); + $oIconField->ValueToDOMNode($oDOMNode, $value); + } else { + parent::PropertyToDOMNode($oDOMNode, $sProperty, $value); + } + } +} diff --git a/sources/Application/Dashlet/Core/DashletHeaderStatic.php b/sources/Application/Dashlet/Core/DashletHeaderStatic.php new file mode 100644 index 000000000..4a8af9112 --- /dev/null +++ b/sources/Application/Dashlet/Core/DashletHeaderStatic.php @@ -0,0 +1,98 @@ + + +namespace Combodo\iTop\Application\Dashlet\Core; + +use Combodo\iTop\Application\Dashlet\Dashlet; +use Combodo\iTop\Application\UI\Base\Component\Dashlet\DashletFactory; +use DesignerForm; +use DesignerTextField; +use Dict; +use utils; + +class DashletHeaderStatic extends Dashlet +{ + /** + * @inheritdoc + */ + public function __construct($oModelReflection, $sId) + { + parent::__construct($oModelReflection, $sId); + $this->aProperties['title'] = Dict::S('UI:DashletHeaderStatic:Prop-Title:Default'); + $oIconSelect = $this->oModelReflection->GetIconSelectionField('icon'); + $this->aProperties['icon'] = $oIconSelect->GetDefaultValue('Contact'); + } + + /** + * @inheritdoc + */ + public function Render($oPage, $bEditMode = false, $aExtraParams = []) + { + $sTitle = $this->aProperties['title']; + $sIcon = $this->aProperties['icon']; + + $oIconSelect = $this->oModelReflection->GetIconSelectionField('icon'); + $sIconPath = ''; + if (utils::IsNotNullOrEmptyString($sIcon)) { + $sIconPath = utils::HtmlEntities($oIconSelect->MakeFileUrl($sIcon)); + } + + return DashletFactory::MakeForDashletHeaderStatic($this->oModelReflection->DictString($sTitle), $sIconPath); + } + + /** + * @inheritdoc + */ + public function GetPropertiesFields(DesignerForm $oForm) + { + $oField = new DesignerTextField('title', Dict::S('UI:DashletHeaderStatic:Prop-Title'), $this->aProperties['title']); + $oForm->AddField($oField); + + $oField = $this->oModelReflection->GetIconSelectionField('icon', Dict::S('UI:DashletHeaderStatic:Prop-Icon'), $this->aProperties['icon']); + $oField->AddAllowedValue(['value' => '', 'label' => Dict::S('UI:DashletIcon:None'), 'icon' => '']); + $oForm->AddField($oField); + } + + /** + * @inheritdoc + */ + protected function PropertyFromDOMNode($oDOMNode, $sProperty) + { + if ($sProperty == 'icon') { + $oIconField = $this->oModelReflection->GetIconSelectionField('icon'); + + return $oIconField->ValueFromDOMNode($oDOMNode); + } else { + return parent::PropertyFromDOMNode($oDOMNode, $sProperty); + } + } + + /** + * @inheritdoc + */ + protected function PropertyToDOMNode($oDOMNode, $sProperty, $value) + { + if ($sProperty == 'icon') { + $oIconField = $this->oModelReflection->GetIconSelectionField('icon'); + $oIconField->ValueToDOMNode($oDOMNode, $value); + } else { + parent::PropertyToDOMNode($oDOMNode, $sProperty, $value); + } + } +} diff --git a/sources/Application/Dashlet/Core/DashletObjectList.php b/sources/Application/Dashlet/Core/DashletObjectList.php new file mode 100644 index 000000000..5e529b5d4 --- /dev/null +++ b/sources/Application/Dashlet/Core/DashletObjectList.php @@ -0,0 +1,176 @@ + + +namespace Combodo\iTop\Application\Dashlet\Core; + +use Combodo\iTop\Application\Dashlet\Dashlet; +use Combodo\iTop\Application\UI\Base\Component\Dashlet\DashletContainer; +use DBObjectSearch; +use DesignerBooleanField; +use DesignerForm; +use DesignerHiddenField; +use DesignerLongTextField; +use DesignerTextField; +use Dict; +use DisplayBlock; +use MetaModel; +use utils; + +class DashletObjectList extends Dashlet +{ + /** + * @inheritdoc + */ + public function __construct($oModelReflection, $sId) + { + parent::__construct($oModelReflection, $sId); + $this->aProperties['title'] = ''; + $this->aProperties['query'] = 'SELECT Contact'; + $this->aProperties['menu'] = false; + } + + /** + * @inheritdoc + * + * @throws \OQLException + * @throws \CoreException + * @throws \ArchivedObjectException + */ + public function Render($oPage, $bEditMode = false, $aExtraParams = []) + { + $sTitle = $this->aProperties['title']; + $sShowMenu = $this->aProperties['menu'] ? '1' : '0'; + $oFilter = $this->GetDBSearch($aExtraParams); + $sClass = $oFilter->GetClass(); + //$oPanel = PanelUIBlockFactory::MakeForClass($sClass, Dict::S($sTitle)) + // ->AddCSSClass('ibo-datatable-panel'); + + $oBlock = new DisplayBlock($oFilter, 'list'); + $aParams = [ + 'menu' => $sShowMenu, + 'table_id' => self::APPUSERPREFERENCES_PREFIX.$this->sId, + 'surround_with_panel' => true, + 'max_height' => '500px', + "panel_title" => Dict::S($sTitle), + "panel_class" => $sClass, + ]; + $sBlockId = 'block_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occurring in the same DOM) + //$oBlock->DisplayIntoContentBlock($oPanel, $oPage, $sBlockId, array_merge($aExtraParams, $aParams)); + + $oPanel = $oBlock->GetDisplay($oPage, $sBlockId, array_merge($aExtraParams, $aParams)); + + return $oPanel; + } + + /** + * @inheritdoc + */ + public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = []) + { + $oDashletContainer = new DashletContainer($this->sId, ['dashlet-content']); + $sTitle = $this->aProperties['title']; + $sQuery = $this->aProperties['query']; + $bShowMenu = $this->aProperties['menu']; + $sHtmlTitle = utils::HtmlEntities($this->oModelReflection->DictString($sTitle)); + if ($sHtmlTitle != '') { + $sHtmlTitle = '

'.$sHtmlTitle.'

'; + } + $oQuery = $this->oModelReflection->GetQuery($sQuery); + $sClass = $oQuery->GetClass(); + $sId = $this->sId; + $sMessage = Dict::S('UI:NoObjectToDisplay'); + $sMenu = ''; + if ($bShowMenu) { + $sMenu = '

'.Dict::Format('UI:ClickToCreateNew', $this->oModelReflection->GetName($sClass)).'

'; + } + + $sHtml = << +

$sHtmlTitle

+
+

$sMessage

+$sMenu +
+ +HTML; + + $oDashletContainer->AddHtml($sHtml); + + return $oDashletContainer; + } + + public function GetDBSearch($aExtraParams = []) + { + $sQuery = $this->aProperties['query']; + if (isset($aExtraParams['query_params'])) { + $aQueryParams = $aExtraParams['query_params']; + } elseif (isset($aExtraParams['this->class']) && isset($aExtraParams['this->id'])) { + $oObj = MetaModel::GetObject($aExtraParams['this->class'], $aExtraParams['this->id']); + $aQueryParams = $oObj->ToArgsForQuery(); + } else { + $aQueryParams = []; + } + + return DBObjectSearch::FromOQL($sQuery, $aQueryParams); + } + + /** + * @inheritdoc + */ + public function GetPropertiesFields(DesignerForm $oForm) + { + $oField = new DesignerTextField('title', Dict::S('UI:DashletObjectList:Prop-Title'), $this->aProperties['title']); + $oForm->AddField($oField); + + $oField = new DesignerLongTextField('query', Dict::S('UI:DashletObjectList:Prop-Query'), $this->aProperties['query']); + $oField->SetMandatory(); + $oField->AddCSSClass("ibo-query-oql"); + $oField->AddCSSClass("ibo-is-code"); + $oForm->AddField($oField); + + $oField = new DesignerBooleanField('menu', Dict::S('UI:DashletObjectList:Prop-Menu'), $this->aProperties['menu']); + $oForm->AddField($oField); + } + + /** + * @inheritdoc + */ + public static function CanCreateFromOQL() + { + return true; + } + + /** + * @inheritdoc + */ + public function GetPropertiesFieldsFromOQL(DesignerForm $oForm, $sOQL = null) + { + $oField = new DesignerTextField('title', Dict::S('UI:DashletObjectList:Prop-Title'), ''); + $oForm->AddField($oField); + + $oField = new DesignerHiddenField('query', Dict::S('UI:DashletObjectList:Prop-Query'), $sOQL); + $oField->SetMandatory(); + $oField->AddCSSClass("ibo-query-oql"); + $oField->AddCSSClass("ibo-is-code"); + $oForm->AddField($oField); + + $oField = new DesignerBooleanField('menu', Dict::S('UI:DashletObjectList:Prop-Menu'), $this->aProperties['menu']); + $oForm->AddField($oField); + } +} diff --git a/sources/Application/Dashlet/Core/DashletPlainText.php b/sources/Application/Dashlet/Core/DashletPlainText.php new file mode 100644 index 000000000..148608093 --- /dev/null +++ b/sources/Application/Dashlet/Core/DashletPlainText.php @@ -0,0 +1,63 @@ + + +namespace Combodo\iTop\Application\Dashlet\Core; + +use Combodo\iTop\Application\Dashlet\Dashlet; +use Combodo\iTop\Application\UI\Base\Component\Dashlet\DashletFactory; +use DesignerForm; +use DesignerLongTextField; +use Dict; +use utils; + +class DashletPlainText extends Dashlet +{ + /** + * @inheritdoc + */ + public function __construct($oModelReflection, $sId) + { + parent::__construct($oModelReflection, $sId); + $this->aProperties['text'] = Dict::S('UI:DashletPlainText:Prop-Text:Default'); + } + + /** + * @inheritdoc + */ + public function Render($oPage, $bEditMode = false, $aExtraParams = []) + { + $sText = $this->aProperties['text']; + $sText = utils::EscapeHtml(Dict::S($sText)); + $sText = str_replace(["\r\n", "\n", "\r"], "
", $sText); + + $sId = 'plaintext_'.($bEditMode ? 'edit_' : '').$this->sId; + + return DashletFactory::MakeForDashletPlainText($sText, $sId); + } + + /** + * @inheritdoc + */ + public function GetPropertiesFields(DesignerForm $oForm) + { + $oField = new DesignerLongTextField('text', Dict::S('UI:DashletPlainText:Prop-Text'), $this->aProperties['text']); + $oField->SetMandatory(); + $oForm->AddField($oField); + } +} diff --git a/sources/Application/Dashlet/Core/DashletProxy.php b/sources/Application/Dashlet/Core/DashletProxy.php new file mode 100644 index 000000000..0b8d67ebf --- /dev/null +++ b/sources/Application/Dashlet/Core/DashletProxy.php @@ -0,0 +1,75 @@ + + +namespace Combodo\iTop\Application\Dashlet\Core; + +use Combodo\iTop\Application\UI\Base\Component\Dashlet\DashletContainer; +use Dict; +use utils; + +class DashletProxy extends DashletUnknown +{ + /** + * @inheritdoc + */ + public function __construct($oModelReflection, $sId) + { + parent::__construct($oModelReflection, $sId); + + // Remove DashletUnknown class + if (($key = array_search('dashlet-unknown', $this->aCSSClasses)) !== false) { + unset($this->aCSSClasses[$key]); + } + + $this->aCSSClasses[] = 'dashlet-proxy'; + } + + /** + * @inheritdoc + */ + public function Render($oPage, $bEditMode = false, $aExtraParams = []) + { + // This should never be called. + $oDashletContainer = new DashletContainer(null, ['dashlet-content']); + $oDashletContainer->AddHtml('
This dashlet is not supposed to be rendered as it is just a proxy for third-party widgets.
'); + + return $oDashletContainer; + } + + /** + * @inheritdoc + * + * @throws \Exception + */ + public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = []) + { + $sIconUrl = utils::HtmlEntities(utils::GetAbsoluteUrlAppRoot().'images/dashlet-proxy.png'); + $sExplainText = Dict::Format('UI:DashletProxy:RenderNoDataText:Edit', $this->GetDashletType()); + + $oDashletContainer = new DashletContainer(null, ['dashlet-content']); + + $sHtml = ''; + $sHtml .= '
'; + $sHtml .= '
'.$sExplainText.'
'; + + $oDashletContainer->AddHtml($sHtml); + + return $oDashletContainer; + } +} diff --git a/sources/Application/Dashlet/Core/DashletUnknown.php b/sources/Application/Dashlet/Core/DashletUnknown.php new file mode 100644 index 000000000..0f670e1c9 --- /dev/null +++ b/sources/Application/Dashlet/Core/DashletUnknown.php @@ -0,0 +1,227 @@ + + +namespace Combodo\iTop\Application\Dashlet\Core; + +use Combodo\iTop\Application\Dashlet\Dashlet; +use Combodo\iTop\Application\UI\Base\Component\Dashlet\DashletContainer; +use DesignerForm; +use DesignerXMLField; +use Dict; +use DOMDocument; +use DOMElement; +use DOMFormatException; +use utils; + +/** + * Class DashletUnknown + * + * Used as a fallback in iTop for unknown dashlet classes. + * + * @since 2.5.0 + */ +class DashletUnknown extends Dashlet +{ + protected static $aClassList = null; + + protected $sOriginalDashletXML; + + /** + * @inheritdoc + */ + public function __construct($oModelReflection, $sId) + { + parent::__construct($oModelReflection, $sId); + $this->sOriginalDashletXML = ''; + $this->aCSSClasses[] = 'dashlet-unknown'; + } + + /** + * @inheritdoc + */ + public function FromDOMNode($oDOMNode) + { + // Parent won't do anything as there is no property declared + parent::FromDOMNode($oDOMNode); + + // Build properties from XML + $this->sOriginalDashletXML = ""; + foreach ($oDOMNode->childNodes as $oDOMChildNode) { + if ($oDOMChildNode instanceof DOMElement) { + $sProperty = $oDOMChildNode->tagName; + + // For all properties but "rank" as it is handle by the dashboard. + if ($sProperty !== 'rank') { + // We need to initialize the property before setting it, otherwise it will guessed as NULL and not used. + $this->aProperties[$sProperty] = ''; + $this->aProperties[$sProperty] = $this->PropertyFromDOMNode($oDOMChildNode, $sProperty); + + // And build the original XML + $this->sOriginalDashletXML .= $oDOMChildNode->ownerDocument->saveXML($oDOMChildNode)."\n"; + } + } + } + + $this->OnUpdate(); + } + + /** + * @inheritdoc + * + * @throws \Exception + * @throws \DOMFormatException + */ + public function ToDOMNode($oDOMNode) + { + $oDoc = new DOMDocument(); + libxml_clear_errors(); + $oDoc->loadXML(''.$this->sOriginalDashletXML.''); + $aErrors = libxml_get_errors(); + if (count($aErrors) > 0) { + throw new DOMFormatException('Dashlet definition not correctly formatted!'); + } + foreach ($oDoc->documentElement->childNodes as $oDOMChildNode) { + $oPropNode = $oDOMNode->ownerDocument->importNode($oDOMChildNode, true); + $oDOMNode->appendChild($oPropNode); + } + } + + /** + * @inheritdoc + * + * @throws \DOMException + */ + public function FromParams($aParams) + { + // For unknown dashlet, parameters are not parsed but passed as a raw xml + if (array_key_exists('xml', $aParams)) { + // A namespace must be present for the "xsi:type" attribute, otherwise a warning will be thrown. + $sXML = ''.$aParams['xml'].''; + $this->FromXml($sXML); + } + $this->OnUpdate(); + } + + /** + * @inheritdoc + * + * @throws \Exception + */ + public function Render($oPage, $bEditMode = false, $aExtraParams = []) + { + $sIconUrl = utils::HtmlEntities(utils::GetAbsoluteUrlAppRoot().'images/dashlet-unknown.png'); + $sExplainText = ($bEditMode) ? Dict::Format('UI:DashletUnknown:RenderText:Edit', $this->GetDashletType()) : Dict::S('UI:DashletUnknown:RenderText:View'); + + $oDashletContainer = new DashletContainer(null, ['dashlet-content']); + + $oDashletContainer->AddHtml('
'.$sExplainText.'
'); + + return $oDashletContainer; + } + + /** + * @inheritdoc + * + * @throws \Exception + */ + public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = []) + { + $sIconUrl = utils::HtmlEntities(utils::GetAbsoluteUrlAppRoot().'images/dashlet-unknown.png'); + $sExplainText = Dict::Format('UI:DashletUnknown:RenderNoDataText:Edit', $this->GetDashletType()); + + $oDashletContainer = new DashletContainer(null, ['dashlet-content']); + + $oDashletContainer->AddHtml('
'.$sExplainText.'
'); + + return $oDashletContainer; + } + + /** + * @inheritdoc + */ + public function GetForm($aInfo = []) + { + if (isset($aInfo['configuration']) && empty($this->sOriginalDashletXML)) { + $this->sOriginalDashletXML = $aInfo['configuration']; + } + + return parent::GetForm($aInfo); + } + + /** + * @inheritdoc + */ + public function GetPropertiesFields(DesignerForm $oForm) + { + $oField = new DesignerXMLField('xml', Dict::S('UI:DashletUnknown:Prop-XMLConfiguration'), $this->sOriginalDashletXML); + $oForm->AddField($oField); + } + + /** + * @inheritdoc + */ + protected function PropertyFromDOMNode($oDOMNode, $sProperty) + { + $bHasSubProperties = false; + foreach ($oDOMNode->childNodes as $oDOMChildNode) { + if ($oDOMChildNode->nodeType === XML_ELEMENT_NODE) { + $bHasSubProperties = true; + break; + } + } + + if ($bHasSubProperties) { + $sTmp = $oDOMNode->ownerDocument->saveXML($oDOMNode, LIBXML_NOENT); + $sTmp = trim(preg_replace("/(<".$oDOMNode->tagName."[^>]*>|<\/".$oDOMNode->tagName.">)/", "", $sTmp)); + + return $sTmp; + } else { + return parent::PropertyFromDOMNode($oDOMNode, $sProperty); + } + } + + /** + * @inheritdoc + */ + protected function PropertyToDOMNode($oDOMNode, $sProperty, $value) + { + // Save subnodes + if (preg_match('/<(.*)>/', $value)) { + /** @var \DOMDocumentFragment $oDOMFragment */ + $oDOMFragment = $oDOMNode->ownerDocument->createDocumentFragment(); + $oDOMFragment->appendXML($value); + $oDOMNode->appendChild($oDOMFragment); + } else { + parent::PropertyToDOMNode($oDOMNode, $sProperty, $value); + } + } + + /** + * @inheritdoc + * + * @throws \DOMException + */ + public function Update($aValues, $aUpdatedFields) + { + $this->FromParams($aValues); + + // OnUpdate() already done in FromParams() + return $this; + } +} diff --git a/sources/Application/Dashlet/Dashlet.php b/sources/Application/Dashlet/Dashlet.php new file mode 100644 index 000000000..2d42a64fc --- /dev/null +++ b/sources/Application/Dashlet/Dashlet.php @@ -0,0 +1,517 @@ + + +namespace Combodo\iTop\Application\Dashlet; + +use Combodo\iTop\Application\UI\Base\Component\Dashlet\DashletContainer; +use Combodo\iTop\Application\UI\Base\iUIBlock; +use Combodo\iTop\Application\UI\Base\UIBlock; +use Combodo\iTop\Application\WebPage\WebPage; +use Combodo\iTop\DesignDocument; +use Combodo\iTop\DesignElement; +use Combodo\iTop\PropertyType\Serializer\XMLNormalizer; +use DesignerForm; +use DesignerHiddenField; +use Dict; +use DOMException; +use DOMNode; +use Exception; +use ModelReflection; +use OQLException; +use UnknownClassOqlException; +use utils; + +/** + * Base class for all 'dashlets' (i.e. widgets to be inserted into a dashboard) + * + * @copyright Copyright (C) 2010-2024 Combodo SAS + * @license http://opensource.org/licenses/AGPL-3.0 + */ +abstract class Dashlet +{ + /** @var string */ + public const APPUSERPREFERENCES_PREFIX = 'Dashlet'; + + protected $oModelReflection; + protected $sId; + protected $bRedrawNeeded; + protected $bFormRedrawNeeded; + protected $aProperties; // array of {property => value} + protected $aCSSClasses; + protected $sDashletType; + protected array $aDefinition; + + /** + * Dashlet constructor. + * + * @param \ModelReflection $oModelReflection + * @param string $sId + * @param string|null $sDashletType + */ + public function __construct(ModelReflection $oModelReflection, $sId) + { + $this->oModelReflection = $oModelReflection; + $this->sId = $sId; + $this->bRedrawNeeded = true; // By default: redraw each time a property changes + $this->bFormRedrawNeeded = false; // By default: no need to redraw the form (independent fields) + $this->aProperties = []; // By default: there is no property + $this->aCSSClasses = ['ibo-dashlet']; + $this->sDashletType = get_class($this); + } + + /** + * Assuming that a property has the type of its default value, set in the constructor + * + * @param string $sProperty + * @param string $sValue + * + * @return mixed + */ + public function Str2Prop($sProperty, $sValue) + { + $refValue = $this->aProperties[$sProperty]; + $sRefType = gettype($refValue); + + if (gettype($sValue) == $sRefType) { + // Do not change anything in that case! + $ret = $sValue; + } elseif ($sRefType == 'boolean') { + $ret = ($sValue == 'true'); + } elseif ($sRefType == 'array') { + $ret = explode(',', $sValue); + } elseif (is_array($sValue)) { + $ret = $sValue; + } else { + $ret = $sValue; + settype($ret, $sRefType); + } + + return $ret; + } + + /** + * @param mixed $value + * + * @return string + */ + public function Prop2Str($value) + { + $sType = gettype($value); + if ($sType == 'boolean') { + $sRet = $value ? 'true' : 'false'; + } elseif ($sType == 'array') { + $sRet = implode(',', $value); + } else { + $sRet = (string)$value; + } + + return $sRet; + } + + protected function OnUpdate() + { + } + + /** + */ + public function FromDOMNode(DesignElement $oDOMNode) + { + foreach ($this->aProperties as $sProperty => $value) { + $oPropNode = $oDOMNode->getElementsByTagName($sProperty)->item(0); + if ($oPropNode != null) { + $this->aProperties[$sProperty] = $this->PropertyFromDOMNode($oPropNode, $sProperty); + } + } + $this->OnUpdate(); + } + + /** + * @param \DOMElement $oDOMNode + */ + public function ToDOMNode($oDOMNode) + { + foreach ($this->aProperties as $sProperty => $value) { + $oPropNode = $oDOMNode->ownerDocument->createElement($sProperty); + $oDOMNode->appendChild($oPropNode); + $this->PropertyToDOMNode($oPropNode, $sProperty, $value); + } + } + + /** + * @param \DOMElement $oDOMNode + * @param string $sProperty + * + * @return mixed + */ + protected function PropertyFromDOMNode($oDOMNode, $sProperty) + { + $res = $this->Str2Prop($sProperty, $oDOMNode->textContent); + + return $res; + } + + /** + * @param \DOMElement $oDOMNode + * @param string $sProperty + * @param mixed $value + */ + protected function PropertyToDOMNode($oDOMNode, $sProperty, $value) + { + $sXmlValue = $this->Prop2Str($value); + $oTextNode = $oDOMNode->ownerDocument->createTextNode($sXmlValue); + $oDOMNode->appendChild($oTextNode); + } + + /** + * @param string $sXml + * + * @throws \DOMException + */ + public function FromXml($sXml) + { + $oDomDoc = new DesignDocument('1.0', 'UTF-8'); + libxml_clear_errors(); + $oDomDoc->loadXml($sXml); + $aErrors = libxml_get_errors(); + if (count($aErrors) > 0) { + throw new DOMException("Malformed XML"); + } + + /** @var DesignElement $oDOMNode */ + $oDOMNode = $oDomDoc->firstChild; + $this->FromDOMNode($oDOMNode); + } + + /** + * @param array $aParams + */ + public function FromParams($aParams) + { + foreach ($this->aProperties as $sProperty => $value) { + if (array_key_exists($sProperty, $aParams)) { + $this->aProperties[$sProperty] = $aParams[$sProperty]; + } + } + $this->OnUpdate(); + } + + public function FromDenormalizedParams(array $aDenormalizedParams) + { + $this->aProperties = XMLNormalizer::GetInstance()->Normalize($aDenormalizedParams, $this->sDashletType, 'Dashlet'); + $this->OnUpdate(); + } + + /** + * @return array Rel. path to the app. root of the JS files required by the dashlet + * @since 3.0.0 + */ + public function GetJSFilesRelPaths(): array + { + return []; + } + + /** + * @return array Rel. path to the app. root of the CSS files required by the dashlet + * @since 3.0.0 + */ + public function GetCSSFilesRelPaths(): array + { + return []; + } + + /** + * @param WebPage $oPage + * @param bool $bEditMode + * @param bool $bEnclosingDiv + * @param array $aExtraParams + */ + public function DoRender($oPage, $bEditMode = false, $bEnclosingDiv = true, $aExtraParams = []): UIBlock + { + $sId = $this->GetID(); + + if ($bEnclosingDiv) { + if ($bEditMode) { + $oDashletContainer = new DashletContainer("dashlet_{$sId}"); + } else { + $oDashletContainer = new DashletContainer(); + } + $oDashletContainer->AddCSSClasses($this->aCSSClasses); + } else { + $oDashletContainer = new DashletContainer(); + $oDashletContainer->AddCSSClasses($this->aCSSClasses); + } + + $oDashletContainer->AddMultipleJsFilesRelPaths($this->GetJSFilesRelPaths()); + $oDashletContainer->AddMultipleCssFilesRelPaths($this->GetCSSFilesRelPaths()); + + try { + if (get_class($this->oModelReflection) == 'ModelReflectionRuntime') { + $oBlock = $this->Render($oPage, $bEditMode, $aExtraParams); + } else { + $oBlock = $this->RenderNoData($oPage, $bEditMode, $aExtraParams); + } + $oDashletContainer->AddSubBlock($oBlock); + } catch (UnknownClassOqlException $e) { + // Maybe the class is part of a non-installed module, fail silently + // Except in Edit mode + if ($bEditMode) { + $oDashletContainer->AddCSSClass("dashlet-content"); + $oDashletContainer->AddHtml('

'.$e->GetUserFriendlyDescription().'

'); + } + } catch (OqlException $e) { + $oDashletContainer->AddCSSClass("dashlet-content"); + $oDashletContainer->AddHtml('

'.utils::HtmlEntities($e->GetUserFriendlyDescription()).'

'); + } catch (Exception $e) { + $oDashletContainer->AddCSSClass("dashlet-content"); + $oDashletContainer->AddHtml('

'.$e->getMessage().'

'); + } + + if ($bEditMode) { + $sClass = $this->sDashletType; + $sType = $this->sDashletType; + $oPage->add_ready_script( + <<sId = $sId; + } + + /** + * @return string + */ + public function GetID() + { + return $this->sId; + } + + /** + * @param WebPage $oPage + * @param bool $bEditMode + * @param array $aExtraParams + * + * @return iUIBlock + */ + abstract public function Render($oPage, $bEditMode = false, $aExtraParams = []); + + /** + * Rendering without the real data + * + * @param WebPage $oPage + * @param bool $bEditMode + * @param array $aExtraParams + * + * @return iUIBlock + */ + public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = []) + { + return $this->Render($oPage, $bEditMode, $aExtraParams); + } + + /** + * @param \DesignerForm $oForm + * + * @return mixed + */ + abstract public function GetPropertiesFields(\DesignerForm $oForm); + + /** + * @param \DOMNode $oContainerNode + */ + public function ToXml(DOMNode $oContainerNode) + { + + } + + /** + * @param array $aValues + * @param array $aUpdatedFields + * + * @return Dashlet + */ + public function Update($aValues, $aUpdatedFields) + { + foreach ($aUpdatedFields as $sProp) { + if (array_key_exists($sProp, $this->aProperties)) { + $this->aProperties[$sProp] = $this->Str2Prop($sProp, $aValues[$sProp]); + } + } + $this->OnUpdate(); + + return $this; + } + + /** + * @return bool + */ + public function IsRedrawNeeded() + { + return $this->bRedrawNeeded; + } + + /** + * @return bool + */ + public function IsFormRedrawNeeded() + { + return $this->bFormRedrawNeeded; + } + + /** + * @param array $aInfo + * + * @return \DesignerForm + */ + public function GetForm($aInfo = []) + { + $oForm = new DesignerForm(); + $sPrefix = "dashlet_".$this->GetID(); + $oForm->SetPrefix($sPrefix); + $oForm->SetHierarchyPath($sPrefix); + $oForm->SetParamsContainer('params'); + + $this->GetPropertiesFields($oForm); + + $oDashletClassField = new DesignerHiddenField('dashlet_class', '', get_class($this)); + $oForm->AddField($oDashletClassField); + + $oDashletTypeField = new DesignerHiddenField('dashlet_type', '', $this->sDashletType); + $oForm->AddField($oDashletTypeField); + + $oDashletIdField = new DesignerHiddenField('dashlet_id', '', $this->GetID()); + $oForm->AddField($oDashletIdField); + + return $oForm; + } + + /** + * @return bool + */ + public static function IsVisible() + { + return true; + } + + /** + * @return bool + */ + public static function CanCreateFromOQL() + { + return false; + } + + /** + * @param \DesignerForm $oForm + * @param string|null $sOQL + */ + public function GetPropertiesFieldsFromOQL(DesignerForm $oForm, $sOQL = null) + { + // Default: do nothing since it's not supported + } + + /** + * @param string $sOql + * + * @return array + */ + protected function GetGroupByOptions($sOql) + { + $aGroupBy = []; + try { + $oQuery = $this->oModelReflection->GetQuery($sOql); + $sClass = $oQuery->GetClass(); + foreach ($this->oModelReflection->ListAttributes($sClass) as $sAttCode => $sAttType) { + // For external fields, find the real type of the target + $sExtFieldAttCode = $sAttCode; + $sTargetClass = $sClass; + while (is_a($sAttType, 'AttributeExternalField', true)) { + $sExtKeyAttCode = $this->oModelReflection->GetAttributeProperty($sTargetClass, $sExtFieldAttCode, 'extkey_attcode'); + $sTargetAttCode = $this->oModelReflection->GetAttributeProperty($sTargetClass, $sExtFieldAttCode, 'target_attcode'); + $sTargetClass = $this->oModelReflection->GetAttributeProperty($sTargetClass, $sExtKeyAttCode, 'targetclass'); + $aTargetAttCodes = $this->oModelReflection->ListAttributes($sTargetClass); + $sAttType = $aTargetAttCodes[$sTargetAttCode]; + $sExtFieldAttCode = $sTargetAttCode; + } + + $aForbidenAttType = [ + 'AttributeLinkedSet', + 'AttributeFriendlyName', + + 'iAttributeNoGroupBy', //we cannot only use iAttributeNoGroupBy since this method is also used by the designer who do not have access to the classes' PHP reflection API. So the known classes has to be listed altogether + 'AttributeOneWayPassword', + 'AttributeEncryptedString', + 'AttributePassword', + ]; + foreach ($aForbidenAttType as $sForbidenAttType) { + if (is_a($sAttType, $sForbidenAttType, true)) { + continue 2; + } + } + + $sLabel = $this->oModelReflection->GetLabel($sClass, $sAttCode); + if (!in_array($sLabel, $aGroupBy)) { + $aGroupBy[$sAttCode] = $sLabel; + + if (is_a($sAttType, 'AttributeDateTime', true)) { + $aGroupBy[$sAttCode.':hour'] = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-Hour', $sLabel); + $aGroupBy[$sAttCode.':month'] = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-Month', $sLabel); + $aGroupBy[$sAttCode.':day_of_week'] = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-DayOfWeek', $sLabel); + $aGroupBy[$sAttCode.':day_of_month'] = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-DayOfMonth', $sLabel); + } + } + } + asort($aGroupBy); + } catch (Exception $e) { + // Fallback in case of OQL problem + } + + return $aGroupBy; + } + + /** + * @return string + */ + public function GetDashletType() + { + return $this->sDashletType; + } + + /** + * @param string $sDashletType + */ + public function SetDashletType($sDashletType) + { + $this->sDashletType = $sDashletType; + } + + public function GetDenormalizedProperties(): ?array + { + return XMLNormalizer::GetInstance()->Denormalize($this->aProperties, $this->sDashletType, 'Dashlet'); + } +} diff --git a/sources/Application/Dashlet/DashletException.php b/sources/Application/Dashlet/DashletException.php new file mode 100644 index 000000000..e8eba0873 --- /dev/null +++ b/sources/Application/Dashlet/DashletException.php @@ -0,0 +1,12 @@ +oModelReflectionRuntime = new ModelReflectionRuntime(); + } + + final public static function GetInstance(): DashletFactory + { + if (!isset(static::$oInstance)) { + static::$oInstance = new DashletFactory(); + } + + return static::$oInstance; + } + + public function SetModelReflectionRuntime(ModelReflectionRuntime $oModelReflectionRuntime): void + { + $this->oModelReflectionRuntime = $oModelReflectionRuntime; + } + + public function CreateDashlet(string $sClass, string $sId): Dashlet + { + if (!DashletService::GetInstance()->IsDashletAvailable($sClass)) { + throw new DashletException("Dashlet ".json_encode($sClass)." is not available"); + } + + /** @var Dashlet $oDashlet */ + $oDashlet = new $sClass($this->oModelReflectionRuntime, $sId); + $oDashlet->SetDashletType($sClass); + + return $oDashlet; + } +} diff --git a/sources/Application/Dashlet/Service/DashletService.php b/sources/Application/Dashlet/Service/DashletService.php new file mode 100644 index 000000000..2c2abccfa --- /dev/null +++ b/sources/Application/Dashlet/Service/DashletService.php @@ -0,0 +1,143 @@ +InitDashletDefinitions(); + + return $this->aDashlets; + } + + /** + * @param string $sXMLContent + * + * @return \Combodo\iTop\DesignElement + * @throws \Combodo\iTop\Application\Dashlet\DashletException + */ + private function GetDomNode(string $sXMLContent): DesignElement + { + $oDoc = new DesignDocument(); + libxml_clear_errors(); + $oDoc->loadXML($sXMLContent); + $aErrors = libxml_get_errors(); + if (count($aErrors) > 0) { + throw new DashletException('Dashlets definition not correctly formatted!'); + } + + /** @var \Combodo\iTop\DesignElement $oRoot */ + $oRoot = $oDoc->firstChild; + + return $oRoot; + } + + /** + * @return string + * @throws \Combodo\iTop\Application\Dashlet\DashletException + */ + private function GetXMLContent(): string + { + $sPath = utils::GetAbsoluteModulePath('core')."dashlets.xml"; + if (!file_exists($sPath)) { + throw new DashletException("Dashlets definition not present"); + } + + return file_get_contents($sPath); + } + + /** + * @param string $sClass + * + * @return bool + * @throws \Combodo\iTop\Application\Dashlet\DashletException + * @throws \DOMFormatException + */ + public function IsDashletAvailable(string $sClass) + { + $this->InitDashletDefinitions(); + + return array_key_exists($sClass, $this->aDashlets); + } + + /** + * @param string $sClass + * + * @return array + * @throws \Combodo\iTop\Application\Dashlet\DashletException + */ + public function GetDashletDefinition(string $sClass): array + { + if ($this->IsDashletAvailable($sClass)) { + return $this->aDashlets[$sClass]; + } + + throw new DashletException('Dashlets definition '.json_encode($sClass).' not present'); + } + + /** + * @return void + * @throws \Combodo\iTop\Application\Dashlet\DashletException + * @throws \DOMFormatException + */ + private function InitDashletDefinitions(): void + { + if (count($this->aDashlets) === 0) { + $this->aDashlets = []; + + $oDashletsNode = $this->GetDomNode($this->GetXMLContent()); + /** @var DesignElement $oDashletNode */ + foreach ($oDashletsNode->getElementsByTagName('dashlet') as $oDashletNode) { + $sType = $oDashletNode->getAttribute('id'); + $aInfo = [ + 'label' => Dict::S($oDashletNode->GetChildText('label')), + 'icon' => $oDashletNode->GetChildText('icon'), + 'description' => Dict::S($oDashletNode->GetChildText('description')), + 'min_width' => intval($oDashletNode->GetChildText('min_width', '2')), + 'min_height' => intval($oDashletNode->GetChildText('min_height', '1')), + 'preferred_width' => intval($oDashletNode->GetChildText('preferred_width', '2')), + 'preferred_height' => intval($oDashletNode->GetChildText('preferred_height', '1')), + 'can_create_by_oql' => boolval($oDashletNode->GetChildText('can_create_by_oql', 'false')), + ]; + $this->aDashlets[$sType] = $aInfo; + } + + uasort($this->aDashlets, function ($a, $b) { + return strcmp($a['label'], $b['label']); + }); + } + } +} diff --git a/sources/Application/Dashlet/dashlet.class.inc.php b/sources/Application/Dashlet/dashlet.class.inc.php deleted file mode 100644 index 577030cef..000000000 --- a/sources/Application/Dashlet/dashlet.class.inc.php +++ /dev/null @@ -1,2397 +0,0 @@ - - -use Combodo\iTop\Application\Helper\WebResourcesHelper; -use Combodo\iTop\Application\UI\Base\Component\Dashlet\DashletContainer; -use Combodo\iTop\Application\UI\Base\Component\Dashlet\DashletFactory; -use Combodo\iTop\Application\UI\Base\Component\Html\Html; -use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory; -use Combodo\iTop\Application\UI\Base\iUIBlock; -use Combodo\iTop\Application\UI\Base\UIBlock; -use Combodo\iTop\Application\WebPage\WebPage; -use Combodo\iTop\DesignElement; -use Combodo\iTop\PropertyType\Serializer\XMLNormalizer; -use Combodo\iTop\PropertyType\Serializer\XMLSerializer; - -require_once(APPROOT.'application/forms.class.inc.php'); - -/** - * Base class for all 'dashlets' (i.e. widgets to be inserted into a dashboard) - * - * @copyright Copyright (C) 2010-2024 Combodo SAS - * @license http://opensource.org/licenses/AGPL-3.0 - */ -abstract class Dashlet -{ - /** @var string */ - public const APPUSERPREFERENCES_PREFIX = 'Dashlet'; - - protected $oModelReflection; - protected $sId; - protected $bRedrawNeeded; - protected $bFormRedrawNeeded; - protected $aProperties; // array of {property => value} - protected $aCSSClasses; - protected $sDashletType; - - /** - * Dashlet constructor. - * - * @param \ModelReflection $oModelReflection - * @param string $sId - */ - public function __construct(ModelReflection $oModelReflection, $sId) - { - $this->oModelReflection = $oModelReflection; - $this->sId = $sId; - $this->bRedrawNeeded = true; // By default: redraw each time a property changes - $this->bFormRedrawNeeded = false; // By default: no need to redraw the form (independent fields) - $this->aProperties = []; // By default: there is no property - $this->aCSSClasses = ['ibo-dashlet']; - $this->sDashletType = get_class($this); - } - - /** - * Assuming that a property has the type of its default value, set in the constructor - * - * @param string $sProperty - * @param string $sValue - * - * @return mixed - */ - public function Str2Prop($sProperty, $sValue) - { - $refValue = $this->aProperties[$sProperty]; - $sRefType = gettype($refValue); - - if (gettype($sValue) == $sRefType) { - // Do not change anything in that case! - $ret = $sValue; - } elseif ($sRefType == 'boolean') { - $ret = ($sValue == 'true'); - } elseif ($sRefType == 'array') { - $ret = explode(',', $sValue); - } elseif (is_array($sValue)) { - $ret = $sValue; - } else { - $ret = $sValue; - settype($ret, $sRefType); - } - - return $ret; - } - - /** - * @param mixed $value - * - * @return string - */ - public function Prop2Str($value) - { - $sType = gettype($value); - if ($sType == 'boolean') { - $sRet = $value ? 'true' : 'false'; - } elseif ($sType == 'array') { - $sRet = implode(',', $value); - } else { - $sRet = (string)$value; - } - - return $sRet; - } - - protected function OnUpdate() - { - } - - /** - */ - public function FromDOMNode(DesignElement $oDOMNode) - { - foreach ($this->aProperties as $sProperty => $value) { - $oPropNode = $oDOMNode->getElementsByTagName($sProperty)->item(0); - if ($oPropNode != null) { - $this->aProperties[$sProperty] = $this->PropertyFromDOMNode($oPropNode, $sProperty); - } - } - $this->OnUpdate(); - } - - /** - * @param \DOMElement $oDOMNode - */ - public function ToDOMNode($oDOMNode) - { - foreach ($this->aProperties as $sProperty => $value) { - $oPropNode = $oDOMNode->ownerDocument->createElement($sProperty); - $oDOMNode->appendChild($oPropNode); - $this->PropertyToDOMNode($oPropNode, $sProperty, $value); - } - } - - /** - * @param \DOMElement $oDOMNode - * @param string $sProperty - * - * @return mixed - */ - protected function PropertyFromDOMNode($oDOMNode, $sProperty) - { - $res = $this->Str2Prop($sProperty, $oDOMNode->textContent); - - return $res; - } - - /** - * @param \DOMElement $oDOMNode - * @param string $sProperty - * @param mixed $value - */ - protected function PropertyToDOMNode($oDOMNode, $sProperty, $value) - { - $sXmlValue = $this->Prop2Str($value); - $oTextNode = $oDOMNode->ownerDocument->createTextNode($sXmlValue); - $oDOMNode->appendChild($oTextNode); - } - - /** - * @param string $sXml - * - * @throws \DOMException - */ - public function FromXml($sXml) - { - $oDomDoc = new DOMDocument('1.0', 'UTF-8'); - libxml_clear_errors(); - $oDomDoc->loadXml($sXml); - $aErrors = libxml_get_errors(); - if (count($aErrors) > 0) { - throw new DOMException("Malformed XML"); - } - - $this->FromDOMNode($oDomDoc->firstChild); - } - - /** - * @param array $aParams - */ - public function FromParams($aParams) - { - foreach ($this->aProperties as $sProperty => $value) { - if (array_key_exists($sProperty, $aParams)) { - $this->aProperties[$sProperty] = $aParams[$sProperty]; - } - } - $this->OnUpdate(); - } - - public function FromDenormalizedParams(array $aDenormalizedParams) - { - $this->aProperties = XMLNormalizer::GetInstance()->Normalize($aDenormalizedParams, get_class($this), 'Dashlet'); - $this->OnUpdate(); - } - - /** - * @return array Rel. path to the app. root of the JS files required by the dashlet - * @since 3.0.0 - */ - public function GetJSFilesRelPaths(): array - { - return []; - } - - /** - * @return array Rel. path to the app. root of the CSS files required by the dashlet - * @since 3.0.0 - */ - public function GetCSSFilesRelPaths(): array - { - return []; - } - - /** - * @param WebPage $oPage - * @param bool $bEditMode - * @param bool $bEnclosingDiv - * @param array $aExtraParams - */ - public function DoRender($oPage, $bEditMode = false, $bEnclosingDiv = true, $aExtraParams = []): UIBlock - { - $sId = $this->GetID(); - - if ($bEnclosingDiv) { - if ($bEditMode) { - $oDashletContainer = new DashletContainer("dashlet_{$sId}"); - } else { - $oDashletContainer = new DashletContainer(); - } - $oDashletContainer->AddCSSClasses($this->aCSSClasses); - } else { - $oDashletContainer = new DashletContainer(); - $oDashletContainer->AddCSSClasses($this->aCSSClasses); - } - - $oDashletContainer->AddMultipleJsFilesRelPaths($this->GetJSFilesRelPaths()); - $oDashletContainer->AddMultipleCssFilesRelPaths($this->GetCSSFilesRelPaths()); - - try { - if (get_class($this->oModelReflection) == 'ModelReflectionRuntime') { - $oBlock = $this->Render($oPage, $bEditMode, $aExtraParams); - } else { - $oBlock = $this->RenderNoData($oPage, $bEditMode, $aExtraParams); - } - $oDashletContainer->AddSubBlock($oBlock); - } catch (UnknownClassOqlException $e) { - // Maybe the class is part of a non-installed module, fail silently - // Except in Edit mode - if ($bEditMode) { - $oDashletContainer->AddCSSClass("dashlet-content"); - $oDashletContainer->AddHtml('

'.$e->GetUserFriendlyDescription().'

'); - } - } catch (OqlException $e) { - $oDashletContainer->AddCSSClass("dashlet-content"); - $oDashletContainer->AddHtml('

'.utils::HtmlEntities($e->GetUserFriendlyDescription()).'

'); - } catch (Exception $e) { - $oDashletContainer->AddCSSClass("dashlet-content"); - $oDashletContainer->AddHtml('

'.$e->getMessage().'

'); - } - - if ($bEditMode) { - $sClass = get_class($this); - $sType = $this->sDashletType; - $oPage->add_ready_script( - <<sId = $sId; - } - - /** - * @return string - */ - public function GetID() - { - return $this->sId; - } - - /** - * @param WebPage $oPage - * @param bool $bEditMode - * @param array $aExtraParams - * - * @return iUIBlock - */ - abstract public function Render($oPage, $bEditMode = false, $aExtraParams = []); - - /** - * Rendering without the real data - * - * @param WebPage $oPage - * @param bool $bEditMode - * @param array $aExtraParams - * - * @return iUIBlock - */ - public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = []) - { - return $this->Render($oPage, $bEditMode, $aExtraParams); - } - - /** - * @param \DesignerForm $oForm - * - * @return mixed - */ - abstract public function GetPropertiesFields(\DesignerForm $oForm); - - /** - * @param \DOMNode $oContainerNode - */ - public function ToXml(DOMNode $oContainerNode) - { - - } - - /** - * @param array $aValues - * @param array $aUpdatedFields - * - * @return \Dashlet - */ - public function Update($aValues, $aUpdatedFields) - { - foreach ($aUpdatedFields as $sProp) { - if (array_key_exists($sProp, $this->aProperties)) { - $this->aProperties[$sProp] = $this->Str2Prop($sProp, $aValues[$sProp]); - } - } - $this->OnUpdate(); - - return $this; - } - - /** - * @return bool - */ - public function IsRedrawNeeded() - { - return $this->bRedrawNeeded; - } - - /** - * @return bool - */ - public function IsFormRedrawNeeded() - { - return $this->bFormRedrawNeeded; - } - - /** - * @return array - */ - public static function GetInfo() - { - return [ - 'label' => '', - 'icon' => '', - 'description' => '', - ]; - } - - /** - * @param array $aInfo - * - * @return \DesignerForm - */ - public function GetForm($aInfo = []) - { - $oForm = new DesignerForm(); - $sPrefix = "dashlet_".$this->GetID(); - $oForm->SetPrefix($sPrefix); - $oForm->SetHierarchyPath($sPrefix); - $oForm->SetParamsContainer('params'); - - $this->GetPropertiesFields($oForm); - - $oDashletClassField = new DesignerHiddenField('dashlet_class', '', get_class($this)); - $oForm->AddField($oDashletClassField); - - $oDashletTypeField = new DesignerHiddenField('dashlet_type', '', $this->sDashletType); - $oForm->AddField($oDashletTypeField); - - $oDashletIdField = new DesignerHiddenField('dashlet_id', '', $this->GetID()); - $oForm->AddField($oDashletIdField); - - return $oForm; - } - - /** - * @return bool - */ - public static function IsVisible() - { - return true; - } - - /** - * @return bool - */ - public static function CanCreateFromOQL() - { - return false; - } - - /** - * @param \DesignerForm $oForm - * @param string|null $sOQL - */ - public function GetPropertiesFieldsFromOQL(DesignerForm $oForm, $sOQL = null) - { - // Default: do nothing since it's not supported - } - - /** - * @param string $sOql - * - * @return array - */ - protected function GetGroupByOptions($sOql) - { - $aGroupBy = []; - try { - $oQuery = $this->oModelReflection->GetQuery($sOql); - $sClass = $oQuery->GetClass(); - foreach ($this->oModelReflection->ListAttributes($sClass) as $sAttCode => $sAttType) { - // For external fields, find the real type of the target - $sExtFieldAttCode = $sAttCode; - $sTargetClass = $sClass; - while (is_a($sAttType, 'AttributeExternalField', true)) { - $sExtKeyAttCode = $this->oModelReflection->GetAttributeProperty($sTargetClass, $sExtFieldAttCode, 'extkey_attcode'); - $sTargetAttCode = $this->oModelReflection->GetAttributeProperty($sTargetClass, $sExtFieldAttCode, 'target_attcode'); - $sTargetClass = $this->oModelReflection->GetAttributeProperty($sTargetClass, $sExtKeyAttCode, 'targetclass'); - $aTargetAttCodes = $this->oModelReflection->ListAttributes($sTargetClass); - $sAttType = $aTargetAttCodes[$sTargetAttCode]; - $sExtFieldAttCode = $sTargetAttCode; - } - - $aForbidenAttType = [ - 'AttributeLinkedSet', - 'AttributeFriendlyName', - - 'iAttributeNoGroupBy', //we cannot only use iAttributeNoGroupBy since this method is also used by the designer who do not have access to the classes' PHP reflection API. So the known classes has to be listed altogether - 'AttributeOneWayPassword', - 'AttributeEncryptedString', - 'AttributePassword', - ]; - foreach ($aForbidenAttType as $sForbidenAttType) { - if (is_a($sAttType, $sForbidenAttType, true)) { - continue 2; - } - } - - $sLabel = $this->oModelReflection->GetLabel($sClass, $sAttCode); - if (!in_array($sLabel, $aGroupBy)) { - $aGroupBy[$sAttCode] = $sLabel; - - if (is_a($sAttType, 'AttributeDateTime', true)) { - $aGroupBy[$sAttCode.':hour'] = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-Hour', $sLabel); - $aGroupBy[$sAttCode.':month'] = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-Month', $sLabel); - $aGroupBy[$sAttCode.':day_of_week'] = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-DayOfWeek', $sLabel); - $aGroupBy[$sAttCode.':day_of_month'] = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-DayOfMonth', $sLabel); - } - } - } - asort($aGroupBy); - } catch (Exception $e) { - // Fallback in case of OQL problem - } - - return $aGroupBy; - } - - /** - * @return string - */ - public function GetDashletType() - { - return $this->sDashletType; - } - - /** - * @param string $sDashletType - */ - public function SetDashletType($sDashletType) - { - $this->sDashletType = $sDashletType; - } - - public function GetDenormalizedProperties(): ?array - { - return XMLNormalizer::GetInstance()->Denormalize($this->aProperties, get_class($this), 'Dashlet'); - } -} - -/** - * Class DashletUnknown - * - * Used as a fallback in iTop for unknown dashlet classes. - * - * @since 2.5.0 - */ -class DashletUnknown extends Dashlet -{ - protected static $aClassList = null; - - protected $sOriginalDashletXML; - - /** - * @inheritdoc - */ - public function __construct($oModelReflection, $sId) - { - parent::__construct($oModelReflection, $sId); - $this->sOriginalDashletXML = ''; - $this->aCSSClasses[] = 'dashlet-unknown'; - } - - /** - * @inheritdoc - */ - public function FromDOMNode($oDOMNode) - { - // Parent won't do anything as there is no property declared - parent::FromDOMNode($oDOMNode); - - // Build properties from XML - $this->sOriginalDashletXML = ""; - foreach ($oDOMNode->childNodes as $oDOMChildNode) { - if ($oDOMChildNode instanceof DOMElement) { - $sProperty = $oDOMChildNode->tagName; - - // For all properties but "rank" as it is handle by the dashboard. - if ($sProperty !== 'rank') { - // We need to initialize the property before setting it, otherwise it will guessed as NULL and not used. - $this->aProperties[$sProperty] = ''; - $this->aProperties[$sProperty] = $this->PropertyFromDOMNode($oDOMChildNode, $sProperty); - - // And build the original XML - $this->sOriginalDashletXML .= $oDOMChildNode->ownerDocument->saveXML($oDOMChildNode)."\n"; - } - } - } - - $this->OnUpdate(); - } - - /** - * @inheritdoc - * - * @throws \Exception - * @throws \DOMFormatException - */ - public function ToDOMNode($oDOMNode) - { - $oDoc = new DOMDocument(); - libxml_clear_errors(); - $oDoc->loadXML(''.$this->sOriginalDashletXML.''); - $aErrors = libxml_get_errors(); - if (count($aErrors) > 0) { - throw new DOMFormatException('Dashlet definition not correctly formatted!'); - } - foreach ($oDoc->documentElement->childNodes as $oDOMChildNode) { - $oPropNode = $oDOMNode->ownerDocument->importNode($oDOMChildNode, true); - $oDOMNode->appendChild($oPropNode); - } - } - - /** - * @inheritdoc - * - * @throws \DOMException - */ - public function FromParams($aParams) - { - // For unknown dashlet, parameters are not parsed but passed as a raw xml - if (array_key_exists('xml', $aParams)) { - // A namespace must be present for the "xsi:type" attribute, otherwise a warning will be thrown. - $sXML = ''.$aParams['xml'].''; - $this->FromXml($sXML); - } - $this->OnUpdate(); - } - - /** - * @inheritdoc - * - * @throws \Exception - */ - public function Render($oPage, $bEditMode = false, $aExtraParams = []) - { - $aInfos = static::GetInfo(); - - $sIconUrl = utils::HtmlEntities(utils::GetAbsoluteUrlAppRoot().$aInfos['icon']); - $sExplainText = ($bEditMode) ? Dict::Format('UI:DashletUnknown:RenderText:Edit', $this->GetDashletType()) : Dict::S('UI:DashletUnknown:RenderText:View'); - - $oDashletContainer = new DashletContainer(null, ['dashlet-content']); - - $oDashletContainer->AddHtml('
'.$sExplainText.'
'); - - return $oDashletContainer; - } - - /** - * @inheritdoc - * - * @throws \Exception - */ - public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = []) - { - $aInfos = static::GetInfo(); - - $sIconUrl = utils::HtmlEntities(utils::GetAbsoluteUrlAppRoot().$aInfos['icon']); - $sExplainText = Dict::Format('UI:DashletUnknown:RenderNoDataText:Edit', $this->GetDashletType()); - - $oDashletContainer = new DashletContainer(null, ['dashlet-content']); - - $oDashletContainer->AddHtml('
'.$sExplainText.'
'); - - return $oDashletContainer; - } - - /** - * @inheritdoc - */ - public function GetForm($aInfo = []) - { - if (isset($aInfo['configuration']) && empty($this->sOriginalDashletXML)) { - $this->sOriginalDashletXML = $aInfo['configuration']; - } - - return parent::GetForm($aInfo); - } - - /** - * @inheritdoc - */ - public function GetPropertiesFields(DesignerForm $oForm) - { - $oField = new DesignerXMLField('xml', Dict::S('UI:DashletUnknown:Prop-XMLConfiguration'), $this->sOriginalDashletXML); - $oForm->AddField($oField); - } - - /** - * @inheritdoc - */ - protected function PropertyFromDOMNode($oDOMNode, $sProperty) - { - $bHasSubProperties = false; - foreach ($oDOMNode->childNodes as $oDOMChildNode) { - if ($oDOMChildNode->nodeType === XML_ELEMENT_NODE) { - $bHasSubProperties = true; - break; - } - } - - if ($bHasSubProperties) { - $sTmp = $oDOMNode->ownerDocument->saveXML($oDOMNode, LIBXML_NOENT); - $sTmp = trim(preg_replace("/(<".$oDOMNode->tagName."[^>]*>|<\/".$oDOMNode->tagName.">)/", "", $sTmp)); - - return $sTmp; - } else { - return parent::PropertyFromDOMNode($oDOMNode, $sProperty); - } - } - - /** - * @inheritdoc - */ - protected function PropertyToDOMNode($oDOMNode, $sProperty, $value) - { - // Save subnodes - if (preg_match('/<(.*)>/', $value)) { - /** @var \DOMDocumentFragment $oDOMFragment */ - $oDOMFragment = $oDOMNode->ownerDocument->createDocumentFragment(); - $oDOMFragment->appendXML($value); - $oDOMNode->appendChild($oDOMFragment); - } else { - parent::PropertyToDOMNode($oDOMNode, $sProperty, $value); - } - } - - /** - * @inheritdoc - * - * @throws \DOMException - */ - public function Update($aValues, $aUpdatedFields) - { - $this->FromParams($aValues); - - // OnUpdate() already done in FromParams() - return $this; - } - - /** - * @inheritdoc - */ - public static function GetInfo() - { - return [ - 'label' => Dict::S('UI:DashletUnknown:Label'), - 'icon' => 'images/dashlet-unknown.png', - 'description' => Dict::S('UI:DashletUnknown:Description'), - ]; - } -} - -class DashletProxy extends DashletUnknown -{ - /** - * @inheritdoc - */ - public function __construct($oModelReflection, $sId) - { - parent::__construct($oModelReflection, $sId); - - // Remove DashletUnknown class - if (($key = array_search('dashlet-unknown', $this->aCSSClasses)) !== false) { - unset($this->aCSSClasses[$key]); - } - - $this->aCSSClasses[] = 'dashlet-proxy'; - } - - /** - * @inheritdoc - */ - public function Render($oPage, $bEditMode = false, $aExtraParams = []) - { - // This should never be called. - $oDashletContainer = new DashletContainer(null, ['dashlet-content']); - $oDashletContainer->AddHtml('
This dashlet is not supposed to be rendered as it is just a proxy for third-party widgets.
'); - - return $oDashletContainer; - } - - /** - * @inheritdoc - * - * @throws \Exception - */ - public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = []) - { - $aInfos = static::GetInfo(); - - $sIconUrl = utils::HtmlEntities(utils::GetAbsoluteUrlAppRoot().$aInfos['icon']); - $sExplainText = Dict::Format('UI:DashletProxy:RenderNoDataText:Edit', $this->GetDashletType()); - - $oDashletContainer = new DashletContainer(null, ['dashlet-content']); - - $sHtml = ''; - $sHtml .= '
'; - $sHtml .= '
'.$sExplainText.'
'; - - $oDashletContainer->AddHtml($sHtml); - - return $oDashletContainer; - } - - /** - * @inheritdoc - */ - public static function GetInfo() - { - return [ - 'label' => Dict::S('UI:DashletProxy:Label'), - 'icon' => 'images/dashlet-proxy.png', - 'description' => Dict::S('UI:DashletProxy:Description'), - ]; - } -} - -class DashletEmptyCell extends Dashlet -{ - /** - * @inheritdoc - */ - public function __construct($oModelReflection, $sId) - { - parent::__construct($oModelReflection, $sId); - } - - /** - * @inheritdoc - */ - public function Render($oPage, $bEditMode = false, $aExtraParams = []) - { - return new Html(' '); - } - - /** - * @inheritdoc - */ - public function GetPropertiesFields(DesignerForm $oForm) - { - } - - /** - * @inheritdoc - */ - public static function GetInfo() - { - return [ - 'label' => 'Empty Cell', - 'icon' => 'images/dashlet-text.png', - 'description' => 'Empty Cell Dashlet Placeholder', - ]; - } - - /** - * @inheritdoc - */ - public static function IsVisible() - { - return false; - } -} - -class DashletPlainText extends Dashlet -{ - /** - * @inheritdoc - */ - public function __construct($oModelReflection, $sId) - { - parent::__construct($oModelReflection, $sId); - $this->aProperties['text'] = Dict::S('UI:DashletPlainText:Prop-Text:Default'); - } - - /** - * @inheritdoc - */ - public function Render($oPage, $bEditMode = false, $aExtraParams = []) - { - $sText = $this->aProperties['text']; - $sText = utils::EscapeHtml(Dict::S($sText)); - $sText = str_replace(["\r\n", "\n", "\r"], "
", $sText); - - $sId = 'plaintext_'.($bEditMode ? 'edit_' : '').$this->sId; - - return DashletFactory::MakeForDashletPlainText($sText, $sId); - } - - /** - * @inheritdoc - */ - public function GetPropertiesFields(DesignerForm $oForm) - { - $oField = new DesignerLongTextField('text', Dict::S('UI:DashletPlainText:Prop-Text'), $this->aProperties['text']); - $oField->SetMandatory(); - $oForm->AddField($oField); - } - - /** - * @inheritdoc - */ - public static function GetInfo() - { - return [ - 'label' => Dict::S('UI:DashletPlainText:Label'), - 'icon' => 'images/dashlets/icons8-text-box-48.png', - 'description' => Dict::S('UI:DashletPlainText:Description'), - ]; - } -} - -class DashletObjectList extends Dashlet -{ - /** - * @inheritdoc - */ - public function __construct($oModelReflection, $sId) - { - parent::__construct($oModelReflection, $sId); - $this->aProperties['title'] = ''; - $this->aProperties['query'] = 'SELECT Contact'; - $this->aProperties['menu'] = false; - } - - /** - * @inheritdoc - * - * @throws \OQLException - * @throws \CoreException - * @throws \ArchivedObjectException - */ - public function Render($oPage, $bEditMode = false, $aExtraParams = []) - { - $sTitle = $this->aProperties['title']; - $sShowMenu = $this->aProperties['menu'] ? '1' : '0'; - $oFilter = $this->GetDBSearch($aExtraParams); - $sClass = $oFilter->GetClass(); - //$oPanel = PanelUIBlockFactory::MakeForClass($sClass, Dict::S($sTitle)) - // ->AddCSSClass('ibo-datatable-panel'); - - $oBlock = new DisplayBlock($oFilter, 'list'); - $aParams = [ - 'menu' => $sShowMenu, - 'table_id' => self::APPUSERPREFERENCES_PREFIX.$this->sId, - 'surround_with_panel' => true, - 'max_height' => '500px', - "panel_title" => Dict::S($sTitle), - "panel_class" => $sClass, - ]; - $sBlockId = 'block_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occurring in the same DOM) - //$oBlock->DisplayIntoContentBlock($oPanel, $oPage, $sBlockId, array_merge($aExtraParams, $aParams)); - - $oPanel = $oBlock->GetDisplay($oPage, $sBlockId, array_merge($aExtraParams, $aParams)); - - return $oPanel; - } - - /** - * @inheritdoc - */ - public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = []) - { - $oDashletContainer = new DashletContainer($this->sId, ['dashlet-content']); - $sTitle = $this->aProperties['title']; - $sQuery = $this->aProperties['query']; - $bShowMenu = $this->aProperties['menu']; - $sHtmlTitle = utils::HtmlEntities($this->oModelReflection->DictString($sTitle)); - if ($sHtmlTitle != '') { - $sHtmlTitle = '

'.$sHtmlTitle.'

'; - } - $oQuery = $this->oModelReflection->GetQuery($sQuery); - $sClass = $oQuery->GetClass(); - $sId = $this->sId; - $sMessage = Dict::S('UI:NoObjectToDisplay'); - $sMenu = ''; - if ($bShowMenu) { - $sMenu = '

'.Dict::Format('UI:ClickToCreateNew', $this->oModelReflection->GetName($sClass)).'

'; - } - - $sHtml = << -

$sHtmlTitle

-
-

$sMessage

-$sMenu -
- -HTML; - - $oDashletContainer->AddHtml($sHtml); - - return $oDashletContainer; - } - - public function GetDBSearch($aExtraParams = []) - { - $sQuery = $this->aProperties['query']; - if (isset($aExtraParams['query_params'])) { - $aQueryParams = $aExtraParams['query_params']; - } elseif (isset($aExtraParams['this->class']) && isset($aExtraParams['this->id'])) { - $oObj = MetaModel::GetObject($aExtraParams['this->class'], $aExtraParams['this->id']); - $aQueryParams = $oObj->ToArgsForQuery(); - } else { - $aQueryParams = []; - } - - return DBObjectSearch::FromOQL($sQuery, $aQueryParams); - } - - /** - * @inheritdoc - */ - public function GetPropertiesFields(DesignerForm $oForm) - { - $oField = new DesignerTextField('title', Dict::S('UI:DashletObjectList:Prop-Title'), $this->aProperties['title']); - $oForm->AddField($oField); - - $oField = new DesignerLongTextField('query', Dict::S('UI:DashletObjectList:Prop-Query'), $this->aProperties['query']); - $oField->SetMandatory(); - $oField->AddCSSClass("ibo-query-oql"); - $oField->AddCSSClass("ibo-is-code"); - $oForm->AddField($oField); - - $oField = new DesignerBooleanField('menu', Dict::S('UI:DashletObjectList:Prop-Menu'), $this->aProperties['menu']); - $oForm->AddField($oField); - } - - /** - * @inheritdoc - */ - public static function GetInfo() - { - return [ - 'label' => Dict::S('UI:DashletObjectList:Label'), - 'icon' => 'images/dashlets/icons8-list-48.png', - 'description' => Dict::S('UI:DashletObjectList:Description'), - 'min_height' => 1, - 'min_width' => 2, - 'preferred_width' => 4, - 'preferred_height' => 3, - ]; - } - - /** - * @inheritdoc - */ - public static function CanCreateFromOQL() - { - return true; - } - - /** - * @inheritdoc - */ - public function GetPropertiesFieldsFromOQL(DesignerForm $oForm, $sOQL = null) - { - $oField = new DesignerTextField('title', Dict::S('UI:DashletObjectList:Prop-Title'), ''); - $oForm->AddField($oField); - - $oField = new DesignerHiddenField('query', Dict::S('UI:DashletObjectList:Prop-Query'), $sOQL); - $oField->SetMandatory(); - $oField->AddCSSClass("ibo-query-oql"); - $oField->AddCSSClass("ibo-is-code"); - $oForm->AddField($oField); - - $oField = new DesignerBooleanField('menu', Dict::S('UI:DashletObjectList:Prop-Menu'), $this->aProperties['menu']); - $oForm->AddField($oField); - } -} - -abstract class DashletGroupBy extends Dashlet -{ - public function __construct($oModelReflection, $sId) - { - parent::__construct($oModelReflection, $sId); - $this->aProperties['title'] = ''; - $this->aProperties['query'] = 'SELECT Contact'; - $this->aProperties['group_by'] = 'status'; - $this->aProperties['style'] = 'table'; - $this->aProperties['aggregation_function'] = 'count'; - $this->aProperties['aggregation_attribute'] = ''; - $this->aProperties['limit'] = ''; - $this->aProperties['order_by'] = ''; - $this->aProperties['order_direction'] = ''; - } - - protected $sGroupByLabel = null; - protected $sGroupByExpr = null; - protected $sGroupByAttCode = null; - protected $sFunction = null; - protected $sAggregationFunction = null; - protected $sAggregationAttribute = null; - protected $sLimit = null; - protected $sOrderBy = null; - protected $sOrderDirection = null; - protected $sClass = null; - - /** - * Compute Grouping - * - * @inheritdoc - */ - public function OnUpdate() - { - $this->sGroupByExpr = null; - $this->sGroupByLabel = null; - $this->sGroupByAttCode = null; - $this->sFunction = null; - $this->sClass = null; - - $sQuery = $this->aProperties['query']; - $sGroupBy = $this->aProperties['group_by']; - - $this->sAggregationFunction = $this->aProperties['aggregation_function']; - $this->sAggregationAttribute = $this->aProperties['aggregation_attribute'] ?? ''; - - $this->sLimit = $this->aProperties['limit'] ?? 0; - $this->sOrderBy = $this->aProperties['order_by'] ?? null; - if (empty($this->sOrderBy)) { - if ($this->aProperties['style'] == 'pie') { - $this->sOrderBy = 'function'; - } else { - $this->sOrderBy = 'attribute'; - } - } - - // First perform the query - if the OQL is not ok, it will generate an exception : no need to go further - try { - $oQuery = $this->oModelReflection->GetQuery($sQuery); - $this->sClass = $oQuery->GetClass(); - $sClassAlias = $oQuery->GetClassAlias(); - } catch (Exception $e) { - // Invalid query, let the user edit the dashlet/dashboard anyhow - $this->sClass = null; - $sClassAlias = ''; - } - - // Check groupby... it can be wrong at this stage - if (preg_match('/^(.*):(.*)$/', $sGroupBy, $aMatches)) { - $this->sGroupByAttCode = $aMatches[1]; - $this->sFunction = $aMatches[2]; - } else { - $this->sGroupByAttCode = $sGroupBy; - $this->sFunction = null; - } - - if ((!is_null($this->sClass)) && empty($this->aProperties['order_direction'])) { - $aAttributeTypes = $this->oModelReflection->ListAttributes($this->sClass); - if (isset($aAttributeTypes[$this->sGroupByAttCode])) { - $sAttributeType = $aAttributeTypes[$this->sGroupByAttCode]; - if (is_subclass_of($sAttributeType, 'AttributeDateTime') || $sAttributeType == 'AttributeDateTime') { - $this->sOrderDirection = 'asc'; - } else { - $this->sOrderDirection = 'desc'; - } - } - } else { - $this->sOrderDirection = $this->aProperties['order_direction']; - } - - if ((!is_null($this->sClass)) && $this->oModelReflection->IsValidAttCode($this->sClass, $this->sGroupByAttCode)) { - $sAttLabel = $this->oModelReflection->GetLabel($this->sClass, $this->sGroupByAttCode); - if (!is_null($this->sFunction)) { - switch ($this->sFunction) { - case 'hour': - $this->sGroupByLabel = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Hour', $sAttLabel); - $this->sGroupByExpr = "DATE_FORMAT($sClassAlias.{$this->sGroupByAttCode}, '%H')"; // 0 -> 23 - break; - - case 'month': - $this->sGroupByLabel = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Month', $sAttLabel); - $this->sGroupByExpr = "DATE_FORMAT($sClassAlias.{$this->sGroupByAttCode}, '%Y-%m')"; // yyyy-mm - break; - - case 'day_of_week': - $this->sGroupByLabel = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:DayOfWeek', $sAttLabel); - $this->sGroupByExpr = "DATE_FORMAT($sClassAlias.{$this->sGroupByAttCode}, '%w')"; - break; - - case 'day_of_month': - $this->sGroupByLabel = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:DayOfMonth', $sAttLabel); - $this->sGroupByExpr = "DATE_FORMAT($sClassAlias.{$this->sGroupByAttCode}, '%Y-%m-%d')"; // mm-dd - break; - - default: - $this->sGroupByLabel = 'Unknown group by function '.$this->sFunction; - $this->sGroupByExpr = $sClassAlias.'.'.$this->sGroupByAttCode; - } - } else { - $this->sGroupByExpr = $sClassAlias.'.'.$this->sGroupByAttCode; - $this->sGroupByLabel = $sAttLabel; - } - } else { - $this->sGroupByAttCode = null; - } - } - - /** - * @inheritdoc - * - * @throws \CoreException - * @throws \ArchivedObjectException - */ - public function Render($oPage, $bEditMode = false, $aExtraParams = []) - { - $sTitle = $this->aProperties['title']; - $sQuery = $this->aProperties['query']; - $sStyle = $this->aProperties['style']; - - // First perform the query - if the OQL is not ok, it will generate an exception : no need to go further - if (isset($aExtraParams['query_params'])) { - $aQueryParams = $aExtraParams['query_params']; - } elseif (isset($aExtraParams['this->class']) && isset($aExtraParams['this->id'])) { - $oObj = MetaModel::GetObject($aExtraParams['this->class'], $aExtraParams['this->id']); - $aQueryParams = $oObj->ToArgsForQuery(); - } else { - $aQueryParams = []; - } - $oFilter = DBObjectSearch::FromOQL($sQuery, $aQueryParams); - $oFilter->SetShowObsoleteData(utils::ShowObsoleteData()); - - $sClass = $oFilter->GetClass(); - if (!$this->oModelReflection->IsValidAttCode($sClass, $this->sGroupByAttCode)) { - return new Html('

'.Dict::S('UI:DashletGroupBy:MissingGroupBy').'

'); - } - - switch ($sStyle) { - case 'bars': - $sType = 'chart'; - $aParams = [ - 'chart_type' => 'bars', - 'chart_title' => $sTitle, - 'group_by' => $this->sGroupByExpr, - 'group_by_label' => $this->sGroupByLabel, - 'aggregation_function' => $this->sAggregationFunction, - 'aggregation_attribute' => $this->sAggregationAttribute, - 'limit' => $this->sLimit, - 'order_direction' => $this->sOrderDirection, - 'order_by' => $this->sOrderBy, - ]; - $sHtmlTitle = ''; // done in the itop block - break; - - case 'pie': - $sType = 'chart'; - $aParams = [ - 'chart_type' => 'pie', - 'chart_title' => $sTitle, - 'group_by' => $this->sGroupByExpr, - 'group_by_label' => $this->sGroupByLabel, - 'aggregation_function' => $this->sAggregationFunction, - 'aggregation_attribute' => $this->sAggregationAttribute, - 'limit' => $this->sLimit, - 'order_direction' => $this->sOrderDirection, - 'order_by' => $this->sOrderBy, - ]; - $sHtmlTitle = ''; // done in the itop block - break; - - case 'table': - default: - $sHtmlTitle = utils::HtmlEntities(Dict::S($sTitle)); // done in the itop block - $sType = 'count'; - $aParams = [ - 'group_by' => $this->sGroupByExpr, - 'group_by_label' => $this->sGroupByLabel, - 'aggregation_function' => $this->sAggregationFunction, - 'aggregation_attribute' => $this->sAggregationAttribute, - 'limit' => $this->sLimit, - 'order_direction' => $this->sOrderDirection, - 'order_by' => $this->sOrderBy, - ]; - break; - } - - //$oPanel = \Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory::MakeStandard(); - //PanelUIBlockFactory::MakeForClass($sClass, Dict::S($sTitle)); - - $sBlockId = 'block_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occurring in the same DOM) - $oBlock = new DisplayBlock($oFilter, $sType); - //$oBlock->DisplayIntoContentBlock($oPanel, $oPage, $sBlockId, array_merge($aExtraParams, $aParams)); - $aExtraParams["surround_with_panel"] = true; - $aExtraParams["panel_title"] = Dict::S($sTitle); - $aExtraParams["panel_class"] = $sClass; - $oPanel = $oBlock->GetDisplay($oPage, $sBlockId, array_merge($aExtraParams, $aParams)); - if ($bEditMode) { - $oPanel->AddHtml('
'); - } - - return $oPanel; - } - - /** - * @return array - */ - protected function MakeSimulatedData() - { - $sQuery = $this->aProperties['query']; - - $oQuery = $this->oModelReflection->GetQuery($sQuery); - $sClass = $oQuery->GetClass(); - - $aDisplayValues = []; - if ($this->oModelReflection->IsValidAttCode($sClass, $this->sGroupByAttCode)) { - $aAttributeTypes = $this->oModelReflection->ListAttributes($sClass); - $sAttributeType = $aAttributeTypes[$this->sGroupByAttCode]; - if (is_subclass_of($sAttributeType, 'AttributeDateTime') || $sAttributeType == 'AttributeDateTime') { - // Note: an alternative to this somewhat hardcoded way of doing things would be to implement... - //$oExpr = Expression::FromOQL($this->sGroupByExpr); - //$aTranslationData = array($oQuery->GetClassAlias() => array($this->sGroupByAttCode => new ScalarExpression(date('Y-m-d H:i:s', $iTime)))); - //$sRawValue = CMDBSource::QueryToScalar('SELECT '.$oExpr->Translate($aTranslationData)->Render()); - //$sValueLabel = $oExpr->MakeValueLabel(oFilter, $sRawValue, $sRawValue); - // Anyhow, this requires : - // - an update to the prototype of MakeValueLabel() so that it takes ModelReflection parameters - // - propose clever date/times samples - - $aValues = []; - switch ($this->sFunction) { - case 'hour': - $aValues = [8, 9, 15, 18]; - break; - - case 'month': - $aValues = ['2013 '.Dict::S('Month-11'), '2013 '.Dict::S('Month-12'), '2014 '.Dict::S('Month-01'), '2014 '.Dict::S('Month-02'), '2014 '.Dict::S('Month-03')]; - break; - - case 'day_of_week': - $aValues = [Dict::S('DayOfWeek-Monday'), Dict::S('DayOfWeek-Wednesday'), Dict::S('DayOfWeek-Thursday'), Dict::S('DayOfWeek-Friday')]; - break; - - case 'day_of_month': - $aValues = [Dict::S('Month-03').' 30', Dict::S('Month-03').' 31', Dict::S('Month-04').' 01', Dict::S('Month-04').' 02', Dict::S('Month-04').' 03']; - break; - } - foreach ($aValues as $sValue) { - $aDisplayValues[] = ['label' => $sValue, 'value' => (int)rand(1, 15)]; - } - } elseif (is_subclass_of($sAttributeType, 'AttributeEnum') || $sAttributeType == 'AttributeEnum') { - $aAllowed = $this->oModelReflection->GetAllowedValues_att($sClass, $this->sGroupByAttCode); - if ($aAllowed) { // null for non enums - foreach ($aAllowed as $sValue => $sValueLabel) { - $iCount = (int)rand(2, 100); - $aDisplayValues[] = [ - 'label' => $sValueLabel, - 'value' => $iCount, - ]; - } - } - } else { - $aDisplayValues[] = ['label' => 'a', 'value' => 123]; - $aDisplayValues[] = ['label' => 'b', 'value' => 321]; - $aDisplayValues[] = ['label' => 'c', 'value' => 456]; - } - } - - return $aDisplayValues; - } - - /** - * @inheritdoc - */ - public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = []) - { - $oDashletContainer = new DashletContainer(null, ['dashlet-content']); - $oDashletContainer->AddHtml('error!'); - - return $oDashletContainer; - } - - /** - * @inheritdoc - */ - public function GetPropertiesFields(DesignerForm $oForm) - { - $oField = new DesignerTextField('title', Dict::S('UI:DashletGroupBy:Prop-Title'), $this->aProperties['title']); - $oForm->AddField($oField); - - $oField = new DesignerLongTextField('query', Dict::S('UI:DashletGroupBy:Prop-Query'), $this->aProperties['query']); - $oField->SetMandatory(); - $oField->AddCSSClass("ibo-query-oql"); - $oField->AddCSSClass("ibo-is-code"); - $oForm->AddField($oField); - - try { - // Group by field: build the list of possible values (attribute codes + ...) - $aGroupBy = $this->GetGroupByOptions($this->aProperties['query']); - - $oField = new DesignerComboField('group_by', Dict::S('UI:DashletGroupBy:Prop-GroupBy'), $this->aProperties['group_by']); - $oField->SetMandatory(); - $oField->SetAllowedValues($aGroupBy); - } catch (Exception $e) { - $oField = new DesignerTextField('group_by', Dict::S('UI:DashletGroupBy:Prop-GroupBy'), $this->aProperties['group_by']); - $oField->SetReadOnly(); - $aGroupBy = []; - } - $oForm->AddField($oField); - - $aStyles = [ - 'pie' => Dict::S('UI:DashletGroupByPie:Label'), - 'bars' => Dict::S('UI:DashletGroupByBars:Label'), - 'table' => Dict::S('UI:DashletGroupByTable:Label'), - ]; - - $oField = new DesignerComboField('style', Dict::S('UI:DashletGroupBy:Prop-Style'), $this->aProperties['style']); - $oField->SetMandatory(); - $oField->SetAllowedValues($aStyles); - $oForm->AddField($oField); - - $aFunctionAttributes = $this->GetNumericAttributes($this->aProperties['query']); - $aFunctions = $this->GetAllowedFunctions($aFunctionAttributes); - $oSelectorField = new DesignerFormSelectorField('aggregation_function', Dict::S('UI:DashletGroupBy:Prop-Function'), $this->aProperties['aggregation_function']); - $oForm->AddField($oSelectorField); - $oSelectorField->SetMandatory(); - // Count sub-menu - $oSubForm = new DesignerForm(); - $oSelectorField->AddSubForm($oSubForm, Dict::S('UI:GroupBy:count'), 'count'); - foreach ($aFunctions as $sFct => $sLabel) { - $oSubForm = new DesignerForm(); - $oField = new DesignerComboField('aggregation_attribute', Dict::S('UI:DashletGroupBy:Prop-FunctionAttribute'), $this->aProperties['aggregation_attribute']); - $oField->SetMandatory(); - $oField->SetAllowedValues($aFunctionAttributes); - $oSubForm->AddField($oField); - $oSelectorField->AddSubForm($oSubForm, $sLabel, $sFct); - } - - $aOrderField = []; - - if (isset($this->aProperties['group_by']) && isset($aGroupBy[$this->aProperties['group_by']])) { - $aOrderField['attribute'] = $aGroupBy[$this->aProperties['group_by']]; - } - - if ($this->aProperties['aggregation_function'] == 'count') { - $aOrderField['function'] = Dict::S('UI:GroupBy:count'); - } else { - $aOrderField['function'] = $aFunctions[$this->aProperties['aggregation_function']]; - } - $oSelectorField = new DesignerFormSelectorField('order_by', Dict::S('UI:DashletGroupBy:Prop-OrderField'), $this->aProperties['order_by']); - $oForm->AddField($oSelectorField); - $oSelectorField->SetMandatory(); - foreach ($aOrderField as $sField => $sLabel) { - $oSubForm = new DesignerForm(); - if ($sField == 'function') { - $oField = new DesignerIntegerField('limit', Dict::S('UI:DashletGroupBy:Prop-Limit'), $this->aProperties['limit']); - $oSubForm->AddField($oField); - } - $oSelectorField->AddSubForm($oSubForm, $sLabel, $sField); - } - - $aOrderDirections = [ - 'asc' => Dict::S('UI:DashletGroupBy:Order:asc'), - 'desc' => Dict::S('UI:DashletGroupBy:Order:desc'), - ]; - $sOrderDirection = empty($this->aProperties['order_direction']) ? $this->sOrderDirection : $this->aProperties['order_direction']; - $oField = new DesignerComboField('order_direction', Dict::S('UI:DashletGroupBy:Prop-OrderDirection'), $sOrderDirection); - $oField->SetMandatory(); - $oField->SetAllowedValues($aOrderDirections); - $oForm->AddField($oField); - - } - - /** - * @return array - */ - protected function GetOrderBy() - { - if (is_null($this->sClass)) { - return []; - } - - return [ - $this->aProperties['group_by'] => $this->oModelReflection->GetLabel($this->sClass, $this->aProperties['group_by']), - '_itop_'.$this->aProperties['aggregation_function'].'_' => Dict::S('UI:GroupBy:'.$this->aProperties['aggregation_function']), - ]; - } - - /** - * @param array $aFunctionAttributes - * - * @return array - */ - protected function GetAllowedFunctions($aFunctionAttributes) - { - $aFunctions = []; - - if (!empty($aFunctionAttributes) || is_null($this->sClass)) { - $aFunctions['sum'] = Dict::S('UI:GroupBy:sum'); - $aFunctions['avg'] = Dict::S('UI:GroupBy:avg'); - $aFunctions['min'] = Dict::S('UI:GroupBy:min'); - $aFunctions['max'] = Dict::S('UI:GroupBy:max'); - } - - return $aFunctions; - } - - /** - * @param string $sOql - * - * @return array - */ - protected function GetNumericAttributes($sOql) - { - $aFunctionAttributes = []; - try { - $oQuery = $this->oModelReflection->GetQuery($sOql); - $sClass = $oQuery->GetClass(); - if (is_null($sClass)) { - return $aFunctionAttributes; - } - foreach ($this->oModelReflection->ListAttributes($sClass) as $sAttCode => $sAttType) { - switch ($sAttType) { - case 'AttributeDecimal': - case 'AttributeDuration': - case 'AttributeInteger': - case 'AttributePercentage': - case 'AttributeSubItem': // TODO: Known limitation: no unit displayed (values in sec) - $sLabel = $this->oModelReflection->GetLabel($sClass, $sAttCode); - $aFunctionAttributes[$sAttCode] = $sLabel; - break; - } - } - } catch (Exception $e) { - // In case the OQL is bad - } - - return $aFunctionAttributes; - } - - /** - * @inheritdoc - */ - public function Update($aValues, $aUpdatedFields) - { - if (in_array('query', $aUpdatedFields)) { - try { - $sCurrQuery = $aValues['query']; - $oCurrSearch = $this->oModelReflection->GetQuery($sCurrQuery); - $sCurrClass = $oCurrSearch->GetClass(); - - $sPrevQuery = $this->aProperties['query']; - $oPrevSearch = $this->oModelReflection->GetQuery($sPrevQuery); - $sPrevClass = $oPrevSearch->GetClass(); - - if ($sCurrClass != $sPrevClass) { - $this->bFormRedrawNeeded = true; - // wrong but not necessary - unset($aUpdatedFields['group_by']); - $this->aProperties['group_by'] = ''; - } - } catch (Exception $e) { - $this->bFormRedrawNeeded = true; - } - } - $oDashlet = parent::Update($aValues, $aUpdatedFields); - - if (in_array('style', $aUpdatedFields)) { - switch ($aValues['style']) { - // Style changed, mutate to the specified type of chart - case 'pie': - $oDashlet = new DashletGroupByPie($this->oModelReflection, $this->sId); - break; - - case 'bars': - $oDashlet = new DashletGroupByBars($this->oModelReflection, $this->sId); - break; - - case 'table': - $oDashlet = new DashletGroupByTable($this->oModelReflection, $this->sId); - break; - } - $oDashlet->FromParams($aValues); - $oDashlet->bRedrawNeeded = true; - $oDashlet->bFormRedrawNeeded = true; - } - if (in_array('aggregation_attribute', $aUpdatedFields) || in_array('order_direction', $aUpdatedFields) || in_array('order_by', $aUpdatedFields) || in_array('limit', $aUpdatedFields)) { - $oDashlet->bRedrawNeeded = true; - } - if (in_array('group_by', $aUpdatedFields) || in_array('aggregation_function', $aUpdatedFields)) { - $oDashlet->bRedrawNeeded = true; - $oDashlet->bFormRedrawNeeded = true; - } - - return $oDashlet; - } - - /** - * @inheritdoc - */ - public static function GetInfo() - { - // Note: no need to translate, should never be visible to the end-user! - return [ - 'label' => 'Objects grouped by...', - 'icon' => 'images/dashlets/icons8-transaction-list-48.png', - 'description' => 'Grouped objects dashlet (abstract)', - ]; - } - - /** - * @inheritdoc - */ - public static function CanCreateFromOQL() - { - return true; - } - - /** - * @inheritdoc - */ - public function GetPropertiesFieldsFromOQL(DesignerForm $oForm, $sOQL = null) - { - $oField = new DesignerTextField('title', Dict::S('UI:DashletGroupBy:Prop-Title'), ''); - $oForm->AddField($oField); - - $oField = new DesignerHiddenField('query', Dict::S('UI:DashletGroupBy:Prop-Query'), $sOQL); - $oField->SetMandatory(); - $oField->AddCSSClass("ibo-query-oql"); - $oField->AddCSSClass("ibo-is-code"); - $oForm->AddField($oField); - - if (!is_null($sOQL)) { - $oField = new DesignerComboField('group_by', Dict::S('UI:DashletGroupBy:Prop-GroupBy'), null); - $aGroupBy = $this->GetGroupByOptions($sOQL); - $oField->SetAllowedValues($aGroupBy); - } else { - // Creating a form for reading parameters! - $oField = new DesignerTextField('group_by', Dict::S('UI:DashletGroupBy:Prop-GroupBy'), null); - } - $oField->SetMandatory(); - - $oForm->AddField($oField); - - $oField = new DesignerHiddenField('style', '', $this->aProperties['style']); - $oField->SetMandatory(); - $oForm->AddField($oField); - } -} - -class DashletGroupByPie extends DashletGroupBy -{ - /** - * @inheritdoc - */ - public function __construct($oModelReflection, $sId) - { - parent::__construct($oModelReflection, $sId); - $this->aProperties['style'] = 'pie'; - } - - /** - * @inheritdoc - */ - public static function GetInfo() - { - return [ - 'label' => Dict::S('UI:DashletGroupByPie:Label'), - 'icon' => 'images/dashlets/icons8-pie-chart-48.png', - 'description' => Dict::S('UI:DashletGroupByPie:Description'), - 'min_height' => 2, - 'min_width' => 2, - 'preferred_width' => 3, - 'preferred_height' => 3, - ]; - } - - /** - * @inheritDoc - */ - public function GetJSFilesRelPaths(): array - { - return array_merge( - parent::GetJSFilesRelPaths(), - WebResourcesHelper::GetJSFilesRelPathsForC3JS() - ); - } - - /** - * @inheritDoc - */ - public function GetCSSFilesRelPaths(): array - { - return array_merge( - parent::GetCSSFilesRelPaths(), - WebResourcesHelper::GetCSSFilesRelPathsForC3JS() - ); - } - - /** - * @inheritdoc - */ - public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = []) - { - $oDashletContainer = new DashletContainer(null, ['dashlet-content']); - - $sTitle = $this->aProperties['title']; - - $sBlockId = 'block_fake_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occuring in the same DOM) - - $HTMLsTitle = ($sTitle != '') ? '

'.utils::HtmlEntities($sTitle).'

' : ''; - $oDashletContainer->AddHtml("
$HTMLsTitle
"); - - $aDisplayValues = $this->MakeSimulatedData(); - - $aColumns = []; - $aNames = []; - foreach ($aDisplayValues as $idx => $aValue) { - $aColumns[] = ['series_'.$idx, (int)$aValue['value']]; - $aNames['series_'.$idx] = $aValue['label']; - } - $sJSColumns = json_encode($aColumns); - $sJSNames = json_encode($aNames); - $oPage->add_ready_script( - <<aProperties['style'] = 'bars'; - } - - /** - * @inheritdoc - */ - public static function GetInfo() - { - return [ - 'label' => Dict::S('UI:DashletGroupByBars:Label'), - 'icon' => 'images/dashlets/icons8-bar-chart-48.png', - 'description' => Dict::S('UI:DashletGroupByBars:Description'), - 'min_height' => 2, - 'min_width' => 2, - 'preferred_width' => 3, - 'preferred_height' => 3, - ]; - } - - /** - * @inheritdoc - */ - public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = []) - { - $oDashletContainer = new DashletContainer(null, ['dashlet-content']); - - $sTitle = $this->aProperties['title']; - - $sBlockId = 'block_fake_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occuring in the same DOM) - - $HTMLsTitle = ($sTitle != '') ? '

'.utils::HtmlEntities($sTitle).'

' : ''; - $oDashletContainer->AddHtml("
$HTMLsTitle
"); - - $aDisplayValues = $this->MakeSimulatedData(); - - $aNames = []; - foreach ($aDisplayValues as $idx => $aValue) { - $aNames[$idx] = $aValue['label']; - } - $sJSNames = json_encode($aNames); - - $sJson = json_encode($aDisplayValues); - $oPage->add_ready_script( - <<aProperties['style'] = 'table'; - } - - /** - * @inheritdoc - */ - public static function GetInfo() - { - return [ - 'label' => Dict::S('UI:DashletGroupByTable:Label'), - 'description' => Dict::S('UI:DashletGroupByTable:Description'), - 'icon' => 'images/dashlets/icons8-transaction-list-48.png', - 'min_height' => 2, - 'min_width' => 2, - 'preferred_width' => 3, - 'preferred_height' => 3, - ]; - } - - /** - * @inheritdoc - */ - public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = []) - { - $oDashletContainer = new DashletContainer(); - - $aDisplayValues = $this->MakeSimulatedData(); - $iTotal = 0; - foreach ($aDisplayValues as $iRow => $aDisplayData) { - $iTotal += $aDisplayData['value']; - } - - $sBlockId = 'block_fake_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occuring in the same DOM) - - $sHtml = ''; - $sHtml .= '
'; - $sHtml .= '
'; - $sHtml .= '

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

'; - $sHtml .= ''; - $sHtml .= ''; - $sHtml .= ''; - $sHtml .= ''; - $sHtml .= ''; - $sHtml .= ''; - $sHtml .= ''; - $sHtml .= ''; - foreach ($aDisplayValues as $aDisplayData) { - $sHtml .= ''; - $sHtml .= ''; - $sHtml .= ''; - $sHtml .= ''; - } - $sHtml .= ''; - $sHtml .= '
'.$this->sGroupByLabel.''.Dict::S('UI:GroupBy:Count').'
'.$aDisplayData['label'].''.$aDisplayData['value'].'
'; - $sHtml .= '
'; - - $sHtml .= '
'; - - $oDashletContainer->AddHtml($sHtml); - - return $oDashletContainer; - } -} - -class DashletHeaderStatic extends Dashlet -{ - /** - * @inheritdoc - */ - public function __construct($oModelReflection, $sId) - { - parent::__construct($oModelReflection, $sId); - $this->aProperties['title'] = Dict::S('UI:DashletHeaderStatic:Prop-Title:Default'); - $oIconSelect = $this->oModelReflection->GetIconSelectionField('icon'); - $this->aProperties['icon'] = $oIconSelect->GetDefaultValue('Contact'); - } - - /** - * @inheritdoc - */ - public function Render($oPage, $bEditMode = false, $aExtraParams = []) - { - $sTitle = $this->aProperties['title']; - $sIcon = $this->aProperties['icon']; - - $oIconSelect = $this->oModelReflection->GetIconSelectionField('icon'); - $sIconPath = ''; - if (Utils::IsNotNullOrEmptyString($sIcon)) { - $sIconPath = utils::HtmlEntities($oIconSelect->MakeFileUrl($sIcon)); - } - - return DashletFactory::MakeForDashletHeaderStatic($this->oModelReflection->DictString($sTitle), $sIconPath); - } - - /** - * @inheritdoc - */ - public function GetPropertiesFields(DesignerForm $oForm) - { - $oField = new DesignerTextField('title', Dict::S('UI:DashletHeaderStatic:Prop-Title'), $this->aProperties['title']); - $oForm->AddField($oField); - - $oField = $this->oModelReflection->GetIconSelectionField('icon', Dict::S('UI:DashletHeaderStatic:Prop-Icon'), $this->aProperties['icon']); - $oField->AddAllowedValue(['value' => '', 'label' => Dict::S('UI:DashletIcon:None'), 'icon' => '']); - $oForm->AddField($oField); - } - - /** - * @inheritdoc - */ - protected function PropertyFromDOMNode($oDOMNode, $sProperty) - { - if ($sProperty == 'icon') { - $oIconField = $this->oModelReflection->GetIconSelectionField('icon'); - - return $oIconField->ValueFromDOMNode($oDOMNode); - } else { - return parent::PropertyFromDOMNode($oDOMNode, $sProperty); - } - } - - /** - * @inheritdoc - */ - protected function PropertyToDOMNode($oDOMNode, $sProperty, $value) - { - if ($sProperty == 'icon') { - $oIconField = $this->oModelReflection->GetIconSelectionField('icon'); - $oIconField->ValueToDOMNode($oDOMNode, $value); - } else { - parent::PropertyToDOMNode($oDOMNode, $sProperty, $value); - } - } - - /** - * @inheritdoc - */ - public static function GetInfo() - { - return [ - 'label' => Dict::S('UI:DashletHeaderStatic:Label'), - 'icon' => 'images/dashlets/icons8-header-48.png', - 'description' => Dict::S('UI:DashletHeaderStatic:Description'), - 'min_height' => 1, - 'min_width' => 3, - 'preferred_width' => 3, - 'preferred_height' => 1, - ]; - } -} - -class DashletHeaderDynamic extends Dashlet -{ - /** - * @inheritdoc - */ - public function __construct($oModelReflection, $sId) - { - parent::__construct($oModelReflection, $sId); - $this->aProperties['title'] = Dict::S('UI:DashletHeaderDynamic:Prop-Title:Default'); - $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'] = ['active', 'inactive']; - } - - /** - * @return array - */ - protected function GetValues() - { - $sQuery = $this->aProperties['query']; - $sGroupBy = $this->aProperties['group_by']; - $aValues = $this->aProperties['values']; - - if (empty($aValues)) { - $aValues = []; - } - - $oQuery = $this->oModelReflection->GetQuery($sQuery); - $sClass = $oQuery->GetClass(); - - if ($this->oModelReflection->IsValidAttCode($sClass, $sGroupBy)) { - if (count($aValues) == 0) { - $aAllowed = $this->oModelReflection->GetAllowedValues_att($sClass, $sGroupBy); - if (is_array($aAllowed)) { - $aValues = array_keys($aAllowed); - } - } - } - - return $aValues; - } - - /** - * @inheritdoc - * - * @throws \CoreException - * @throws \ArchivedObjectException - */ - public function Render($oPage, $bEditMode = false, $aExtraParams = []) - { - $sTitle = utils::HtmlEntities($this->aProperties['title']); - $sIcon = $this->aProperties['icon']; - $sSubtitle = utils::HtmlEntities($this->aProperties['subtitle']); - $sQuery = $this->aProperties['query']; - $sGroupBy = $this->aProperties['group_by']; - - $oIconSelect = $this->oModelReflection->GetIconSelectionField('icon'); - $sIconPath = ''; - if (Utils::IsNotNullOrEmptyString($sIcon)) { - $sIconPath = $oIconSelect->MakeFileUrl($sIcon); - } - - $aValues = $this->GetValues(); - if (count($aValues) > 0) { - // Stats grouped by - $sCSV = implode(',', $aValues); - $aParams = [ - 'title[block]' => $sTitle, - 'label[block]' => $sSubtitle, - 'status[block]' => $sGroupBy, - 'status_codes[block]' => $sCSV, - 'context_filter' => 1, - ]; - } else { - // Simple stats - $aParams = [ - 'title[block]' => $sTitle, - 'label[block]' => $sSubtitle, - 'context_filter' => 1, - ]; - } - - if (isset($aExtraParams['query_params'])) { - $aQueryParams = $aExtraParams['query_params']; - } elseif (isset($aExtraParams['this->class'])) { - $oObj = MetaModel::GetObject($aExtraParams['this->class'], $aExtraParams['this->id']); - $aQueryParams = $oObj->ToArgsForQuery(); - } else { - $aQueryParams = []; - } - $oFilter = DBObjectSearch::FromOQL($sQuery, $aQueryParams); - $oFilter->SetShowObsoleteData(utils::ShowObsoleteData()); - $sClass = $oFilter->GetClass(); - - $oPanel = PanelUIBlockFactory::MakeNeutral(Dict::S(str_replace('_', ':', $sTitle))) - ->SetIcon($sIconPath) - ->SetColorFromClass($sClass); - $oBlock = new DisplayBlock($oFilter, 'summary'); - $sBlockId = 'block_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occuring in the same DOM) - $oBlock->DisplayIntoContentBlock($oPanel, $oPage, $sBlockId, array_merge($aExtraParams, $aParams)); - - $oSubTitle = $oPanel->GetSubTitleBlock(); - $oSet = new DBObjectSet($oFilter); - $iCount = $oSet->Count(); - $oAppContext = new ApplicationContext(); - $sHyperlink = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=search'.$oAppContext->GetForLink(true).'&filter='.rawurlencode($oFilter->serialize()); - $oSubTitle->AddHtml(''.Dict::Format(str_replace('_', ':', $sSubtitle), $iCount).''); - - return $oPanel; - } - - /** - * @inheritdoc - */ - public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = []) - { - $sTitle = utils::HtmlEntities($this->aProperties['title']); - $sIcon = $this->aProperties['icon']; - $sSubtitle = utils::HtmlEntities($this->aProperties['subtitle']); - $sQuery = $this->aProperties['query']; - $sGroupBy = $this->aProperties['group_by']; - - $aValueLabels = []; - $aValues = []; - try { - $oQuery = $this->oModelReflection->GetQuery($sQuery); - $sClass = $oQuery->GetClass(); - $aValues = $this->GetValues(); - foreach ($aValues as $sValue) { - $aValueLabels[] = $this->oModelReflection->GetValueLabel($sClass, $sGroupBy, $sValue); - } - } catch (UnknownClassOqlException $e) { - $aValueLabels[] = $e->GetUserFriendlyDescription(); - $aValues[] = 1; - } - - $oIconSelect = $this->oModelReflection->GetIconSelectionField('icon'); - $sIconPath = utils::HtmlEntities($oIconSelect->MakeFileUrl($sIcon)); - - $oDashletContainer = new DashletContainer(null, ['dashlet-content']); - - $sHtml = ''; - $sHtml .= ''; - - $sBlockId = 'block_fake_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occuring in the same DOM) - - $iTotal = 0; - - $sHtml .= '
'; - $sHtml .= '
'; - $sHtml .= ''; - $sHtml .= ''; - foreach ($aValueLabels as $sValueLabel) { - $sHtml .= ' '; - } - $sHtml .= ''; - $sHtml .= ''; - foreach ($aValues as $sValue) { - $iCount = rand(2, 100); - $iTotal += $iCount; - $sHtml .= ' '; - } - $sHtml .= ''; - $sHtml .= '
'.$sValueLabel.'
'.$iCount.'
'; - $sHtml .= '
'; - - $sTitle = $this->oModelReflection->DictString($sTitle); - $sSubtitle = $this->oModelReflection->DictFormat($sSubtitle, $iTotal); - - $sHtml .= '

'.utils::HtmlEntities($sTitle).'

'; - $sHtml .= ''.utils::HtmlEntities($sSubtitle).''; - $sHtml .= '
'; - - $oDashletContainer->AddHtml($sHtml); - - return $oDashletContainer; - - } - - /** - * @inheritdoc - */ - public function GetPropertiesFields(DesignerForm $oForm) - { - $oField = new DesignerTextField('title', Dict::S('UI:DashletHeaderDynamic:Prop-Title'), $this->aProperties['title']); - $oForm->AddField($oField); - - $oField = $this->oModelReflection->GetIconSelectionField('icon', Dict::S('UI:DashletHeaderDynamic:Prop-Icon'), $this->aProperties['icon']); - $oField->AddAllowedValue(['value' => '', 'label' => Dict::S('UI:DashletIcon:None'), 'icon' => '']); - $oForm->AddField($oField); - - $oField = new DesignerTextField('subtitle', Dict::S('UI:DashletHeaderDynamic:Prop-Subtitle'), $this->aProperties['subtitle']); - $oForm->AddField($oField); - - $oField = new DesignerLongTextField('query', Dict::S('UI:DashletHeaderDynamic:Prop-Query'), $this->aProperties['query']); - $oField->SetMandatory(); - $oField->AddCSSClass("ibo-query-oql"); - $oField->AddCSSClass("ibo-is-code"); - $oForm->AddField($oField); - - try { - // Group by field: build the list of possible values (attribute codes + ...) - $oQuery = $this->oModelReflection->GetQuery($this->aProperties['query']); - $sClass = $oQuery->GetClass(); - $aGroupBy = $this->GetGroupByOptions($this->aProperties['query']); - $oField = new DesignerComboField('group_by', Dict::S('UI:DashletHeaderDynamic:Prop-GroupBy'), $this->aProperties['group_by']); - $oField->SetMandatory(); - $oField->SetAllowedValues($aGroupBy); - } catch (Exception $e) { - $oField = new DesignerTextField('group_by', Dict::S('UI:DashletHeaderDynamic:Prop-GroupBy'), $this->aProperties['group_by']); - $oField->SetReadOnly(); - } - $oForm->AddField($oField); - - $oField = new DesignerComboField('values', Dict::S('UI:DashletHeaderDynamic:Prop-Values'), $this->aProperties['values']); - $oField->MultipleSelection(true); - if (isset($sClass) && $this->oModelReflection->IsValidAttCode($sClass, $this->aProperties['group_by'])) { - $aValues = $this->oModelReflection->GetAllowedValues_att($sClass, $this->aProperties['group_by']); - $oField->SetAllowedValues($aValues); - } else { - $oField->SetReadOnly(); - } - $oForm->AddField($oField); - } - - /** - * @inheritdoc - */ - public function Update($aValues, $aUpdatedFields) - { - if (in_array('query', $aUpdatedFields)) { - try { - $sCurrQuery = $aValues['query']; - $oCurrSearch = $this->oModelReflection->GetQuery($sCurrQuery); - $sCurrClass = $oCurrSearch->GetClass(); - - $sPrevQuery = $this->aProperties['query']; - $oPrevSearch = $this->oModelReflection->GetQuery($sPrevQuery); - $sPrevClass = $oPrevSearch->GetClass(); - - if ($sCurrClass != $sPrevClass) { - $this->bFormRedrawNeeded = true; - // wrong but not necessary - unset($aUpdatedFields['group_by']); - $this->aProperties['group_by'] = ''; - $this->aProperties['values'] = []; - } - } catch (Exception $e) { - $this->bFormRedrawNeeded = true; - } - } - if (in_array('group_by', $aUpdatedFields)) { - $this->bFormRedrawNeeded = true; - $this->aProperties['values'] = []; - } - - return parent::Update($aValues, $aUpdatedFields); - } - - /** - * @inheritdoc - */ - protected function PropertyFromDOMNode($oDOMNode, $sProperty) - { - if ($sProperty == 'icon') { - $oIconField = $this->oModelReflection->GetIconSelectionField('icon'); - - return $oIconField->ValueFromDOMNode($oDOMNode); - } else { - return parent::PropertyFromDOMNode($oDOMNode, $sProperty); - } - } - - /** - * @inheritdoc - */ - protected function PropertyToDOMNode($oDOMNode, $sProperty, $value) - { - if ($sProperty == 'icon') { - $oIconField = $this->oModelReflection->GetIconSelectionField('icon'); - $oIconField->ValueToDOMNode($oDOMNode, $value); - } else { - parent::PropertyToDOMNode($oDOMNode, $sProperty, $value); - } - } - - /** - * @inheritdoc - */ - public static function GetInfo() - { - return [ - 'label' => Dict::S('UI:DashletHeaderDynamic:Label'), - 'icon' => 'images/dashlets/icons8-header-altered-48.png', - 'description' => Dict::S('UI:DashletHeaderDynamic:Description'), - 'min_height' => 1, - 'min_width' => 2, - 'preferred_width' => 4, - 'preferred_height' => 3, - ]; - } -} - -class DashletBadge extends Dashlet -{ - /** - * @inheritdoc - */ - public function __construct($oModelReflection, $sId) - { - parent::__construct($oModelReflection, $sId); - $this->aProperties['class'] = 'Contact'; - $this->aCSSClasses[] = 'ibo-dashlet--is-inline'; - $this->aCSSClasses[] = 'ibo-dashlet-badge'; - } - - /** - * @inheritdoc - * - * @throws \Exception - */ - public function Render($oPage, $bEditMode = false, $aExtraParams = []) - { - $oDashletContainer = new DashletContainer($this->sId, ['dashlet-content']); - - $sClass = $this->aProperties['class']; - $oFilter = new DBObjectSearch($sClass); - $oBlock = new DisplayBlock($oFilter, 'actions'); - $aExtraParams['context_filter'] = 1; - $aExtraParams['withJSRefreshCallBack'] = true; - $sBlockId = 'block_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occurring in the same DOM) - $oBlock->DisplayIntoContentBlock($oDashletContainer, $oPage, $sBlockId, $aExtraParams); - - return $oDashletContainer; - } - - /** - * @inheritdoc - */ - public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = []) - { - $oDashletContainer = new DashletContainer($this->sId, ['dashlet-content']); - - $sClass = $this->aProperties['class']; - $sIconUrl = utils::HtmlEntities($this->oModelReflection->GetClassIcon($sClass, false)); - $sClassLabel = $this->oModelReflection->GetName($sClass); - $sId = $this->sId; - $sClassCreate = Dict::Format('UI:ClickToCreateNew', $sClassLabel); - - $sHtml = << - - -HTML; - - $oDashletContainer->AddHtml($sHtml); - - return $oDashletContainer; - } - - protected static $aClassList = null; - - /** - * @inheritdoc - * - * @throws \Exception - */ - public function GetPropertiesFields(DesignerForm $oForm) - { - if (is_null(self::$aClassList)) { - // Cache the ordered list of classes (ordered on the label) - // (has a significant impact when editing a page with lots of badges) - // - $aClasses = []; - foreach ($this->oModelReflection->GetClasses('bizmodel', true /*exclude links*/) as $sClass) { - $aClasses[$sClass] = $this->oModelReflection->GetName($sClass); - } - asort($aClasses); - - self::$aClassList = []; - foreach ($aClasses as $sClass => $sLabel) { - $sIconUrl = $this->oModelReflection->GetClassIcon($sClass, false); - if ($sIconUrl == '') { - // The icon does not exist, let's use a transparent one of the same size. - $sIconUrl = utils::GetAbsoluteUrlAppRoot().'images/transparent_32_32.png'; - } - self::$aClassList[] = ['value' => $sClass, 'label' => $sLabel, 'icon' => $sIconUrl]; - } - } - - $oField = new DesignerIconSelectionField('class', Dict::S('UI:DashletBadge:Prop-Class'), $this->aProperties['class']); - $oField->SetAllowedValues(self::$aClassList); - - $oForm->AddField($oField); - } - - /** - * @inheritdoc - */ - public static function GetInfo() - { - return [ - 'label' => Dict::S('UI:DashletBadge:Label'), - 'icon' => 'images/dashlets/icons8-badge-48.png', - 'description' => Dict::S('UI:DashletBadge:Description'), - 'min_height' => 1, - 'min_width' => 2, - 'preferred_width' => 2, - 'preferred_height' => 1, - ]; - } -} diff --git a/sources/Application/UI/Base/Layout/DashletPanel/DashletPanelFactory.php b/sources/Application/UI/Base/Layout/DashletPanel/DashletPanelFactory.php index 404b10a25..6c30900e6 100644 --- a/sources/Application/UI/Base/Layout/DashletPanel/DashletPanelFactory.php +++ b/sources/Application/UI/Base/Layout/DashletPanel/DashletPanelFactory.php @@ -2,7 +2,7 @@ namespace Combodo\iTop\Application\UI\Base\Layout\DashletPanel; -use Combodo\iTop\Service\Dashboard\DashletService; +use Combodo\iTop\Application\Dashlet\Service\DashletService; class DashletPanelFactory { @@ -10,7 +10,7 @@ class DashletPanelFactory { $oDashletPanel = new DashletPanel($sId); - $aAvailableDashlets = DashletService::GetAvailableDashlets(); + $aAvailableDashlets = DashletService::GetInstance()->GetAvailableDashlets(); foreach ($aAvailableDashlets as $sDashletClass => $aDashletInformation) { $oDashletEntry = new DashletEntry($sDashletClass, $aDashletInformation['label'], $aDashletInformation['description'], $aDashletInformation['icon']); diff --git a/sources/Service/Dashboard/DashletService.php b/sources/Service/Dashboard/DashletService.php deleted file mode 100644 index 10acc3f77..000000000 --- a/sources/Service/Dashboard/DashletService.php +++ /dev/null @@ -1,31 +0,0 @@ -isAbstract()) { - $aCallSpec = [$sDashletClass, 'IsVisible']; - $bVisible = call_user_func($aCallSpec); - if ($bVisible) { - $aCallSpec = [$sDashletClass, 'GetInfo']; - $aInfo = call_user_func($aCallSpec); - $aDashlets[$sDashletClass] = $aInfo; - } - } - } - } - - return $aDashlets; - } -} diff --git a/sources/alias.php b/sources/alias.php index 1ab3d41a4..378a0a252 100644 --- a/sources/alias.php +++ b/sources/alias.php @@ -97,6 +97,19 @@ class_alias(\Combodo\iTop\Core\AttributeDefinition\AttributeURL::class, 'Attribu class_alias(\Combodo\iTop\Core\AttributeDefinition\iAttributeNoGroupBy::class, 'iAttributeNoGroupBy'); class_alias(\Combodo\iTop\Core\AttributeDefinition\MissingColumnException::class, 'MissingColumnException'); +class_alias(\Combodo\iTop\Application\Dashlet\Dashlet::class, 'Dashlet'); +class_alias(\Combodo\iTop\Application\Dashlet\Core\DashletBadge::class, 'DashletBadge'); +class_alias(\Combodo\iTop\Application\Dashlet\Core\DashletGroupBy::class, 'DashletGroupBy'); +class_alias(\Combodo\iTop\Application\Dashlet\Core\DashletGroupByBars::class, 'DashletGroupByBars'); +class_alias(\Combodo\iTop\Application\Dashlet\Core\DashletGroupByPie::class, 'DashletGroupByPie'); +class_alias(\Combodo\iTop\Application\Dashlet\Core\DashletGroupByTable::class, 'DashletGroupByTable'); +class_alias(\Combodo\iTop\Application\Dashlet\Core\DashletHeaderDynamic::class, 'DashletHeaderDynamic'); +class_alias(\Combodo\iTop\Application\Dashlet\Core\DashletHeaderStatic::class, 'DashletHeaderStatic'); +class_alias(\Combodo\iTop\Application\Dashlet\Core\DashletObjectList::class, 'DashletObjectList'); +class_alias(\Combodo\iTop\Application\Dashlet\Core\DashletPlainText::class, 'DashletPlainText'); +class_alias(\Combodo\iTop\Application\Dashlet\Core\DashletProxy::class, 'DashletProxy'); +class_alias(\Combodo\iTop\Application\Dashlet\Core\DashletUnknown::class, 'DashletUnknown'); + class_alias(\Combodo\iTop\PropertyType\PropertyType::class, 'Combodo-PropertyType'); class_alias(\Combodo\iTop\PropertyType\ValueType\Branch\ValueTypeCollection::class, 'Combodo-ValueType-Collection');