diff --git a/js/layouts/dashboard/dashboard.js b/js/layouts/dashboard/dashboard.js index 04e2b2c33..a36076fa4 100644 --- a/js/layouts/dashboard/dashboard.js +++ b/js/layouts/dashboard/dashboard.js @@ -22,6 +22,11 @@ class IboDashboard extends HTMLElement { this.csrfToken = null; /** @type {number} Payload schema version */ this.schemaVersion = 2; + /** @type {boolean} Define is the current is dashboard is custom or not */ + this.bIsCustomDashboard = false; + // TODO 3.3 Do not use file that come from frontend + /** @type {string} File for the default dashboard */ + this.sFile = ''; /** @type {object|null} Last saved state for cancel functionality, unused yet */ this.aLastSavedState = null; @@ -29,7 +34,10 @@ class IboDashboard extends HTMLElement { connectedCallback() { this.sId = this.getAttribute("id"); - this.bEditMode = (this.getAttribute("data-edit-mode") === "edit"); + this.bEditMode = (this.getAttribute("data-edit-mode") === "edit") + this.bIsCustomDashboard = this.getAttribute("data-is-custom") === "true"; + this.sFile = this.getAttribute("data-file") || ''; + this.SetupGrid(); this.BindEvents(); @@ -55,6 +63,10 @@ class IboDashboard extends HTMLElement { this.SetEditMode(false); }); + document.querySelector('.ibo-dashboard--selector[data-dashboard-id="'+this.sId+'"] input[type="checkbox"]')?.addEventListener('change', (e) => { + const bIsCustomDashboard = e.target.checked; + this.SetIsCustomDashboard(bIsCustomDashboard); + }); // TODO 3.3 Add event listener to dashboard toggler to get custom/default dashboard switching // TODO 3.3 require load method that's not finished yet @@ -69,27 +81,37 @@ class IboDashboard extends HTMLElement { this.oGrid = this.querySelector('ibo-dashboard-grid'); } - + async SetIsCustomDashboard(bIsCustom) { + this.bIsCustomDashboard = bIsCustom; + this.setAttribute("data-custom-dashboard", bIsCustom ? "true" : "false"); + SetUserPreference(`display_original_dashboard_${this.sId}`, !bIsCustom, true); + console.log(document.querySelector('.ibo-dashboard--selector[data-dashboard-id="'+this.sId+'"] input[type="checkbox"]')); + document.querySelector('.ibo-dashboard--selector[data-dashboard-id="'+this.sId+'"] input[type="checkbox"]').checked = bIsCustom; + return this.ReloadFromBackend(bIsCustom); + } GetEditMode() { return this.bEditMode; } ToggleEditMode(){ - this.SetEditMode(!this.bEditMode); + return this.SetEditMode(!this.bEditMode); } - SetEditMode(bEditMode) { + async SetEditMode(bEditMode) { + if (this.bIsCustomDashboard === false && bEditMode === true) { + await this.SetIsCustomDashboard(true); + } + this.bEditMode = bEditMode; this.oGrid.SetEditable(this.bEditMode); - if(this.bEditMode){ + if (this.bEditMode) { // TODO 3.3 If we are in default dashboard display, change to custom to allow editing // TODO 3.3 Get the custom dashboard and load it, show a tooltip on the dashboard toggler to explain that we switched to custom mode this.aLastSavedState = this.Serialize(); this.setAttribute("data-edit-mode", "edit"); - } - else{ + } else { this.setAttribute("data-edit-mode", "view"); } } @@ -292,12 +314,16 @@ class IboDashboard extends HTMLElement { this.oGrid.RemoveDashlet(sDashletId); } - RefreshFromBackend(bCustomDashboard = false) { - const sLoadDashboardUrl = GetAbsoluteUrlAppRoot() + `/pages/UI.php?route=dashboard.load&id=${this.sId}&custom=${bCustomDashboard ? 'true' : 'false'}`; + ReloadFromBackend(bCustomDashboard = false) { + let sLoadDashboardUrl = GetAbsoluteUrlAppRoot() + `/pages/UI.php?route=dashboard.load&id=${this.sId}&is_custom=${bCustomDashboard ? 'true' : 'false'}`; + if(!bCustomDashboard && this.sFile.length > 0) { + sLoadDashboardUrl += `&file=${encodeURIComponent(this.sFile)}`; + } + fetch(sLoadDashboardUrl) .then(async oResponse => { const oDashletData = await oResponse.json(); - this.Load(oDashletData); + this.Load(oDashletData.data); } ) } @@ -332,7 +358,7 @@ class IboDashboard extends HTMLElement { if(res.status === 'ok') { CombodoToast.OpenToast(res.message, 'success'); this.aLastSavedState = this.Serialize(); - this.SetEditMode(false); + await this.SetEditMode(false); } else { CombodoToast.OpenToast(res.message, 'error'); } @@ -342,14 +368,14 @@ class IboDashboard extends HTMLElement { Load(aSaveState) { try { + // TODO 3.3 Maybe we won't need to validate schema version right now as we control both sides // Validate schema version - if (aSaveState.schema_version !== this.schemaVersion) { + if (false && aSaveState.schema_version !== this.schemaVersion) { CombodoToast.OpenToast('Somehow, we got an incompatible dashboard schema version.', 'error'); return false; } // Update dashboard data - this.sId = aSaveState.id; this.sTitle = aSaveState.title || ""; this.iRefreshRate = parseInt(aSaveState.refresh, 10) || 0; @@ -376,14 +402,12 @@ class IboDashboard extends HTMLElement { const iWidth = aDashletData.width; const iHeight = aDashletData.height; const aDashlet = aDashletData.dashlet; + let sDashletHtml = ''; + // Check if the dashlet state has HTML content - // We need to fetch dashlet HTML from server as scripts need to be executed again - let oGetDashletPromise = this.GetDashlet(aDashlet.type, aDashlet.id, JSON.stringify(aDashlet.properties)); - - - oGetDashletPromise.then(async data => { - let sDashletHtml = await data.text(); - // Add dashlet to grid with its position and size + // TODO 3.3 Is there a way to avoid duplicating AddDashlet call but keep the promise to avoid waiting for fetch result in this loop ? + if(aDashletData.html && aDashletData.html.length > 0) { + sDashletHtml = aDashletData.html; this.oGrid.AddDashlet(sDashletHtml, { x: iPosX, y: iPosY, @@ -391,7 +415,22 @@ class IboDashboard extends HTMLElement { h: iHeight, autoPosition: false }); - }); + } else { + // We need to fetch dashlet HTML from server as scripts need to be executed again + let oGetDashletPromise = this.GetDashlet(aDashlet.type, aDashlet.id, JSON.stringify(aDashlet.properties)); + + oGetDashletPromise.then(async data => { + let sDashletHtml = await data.text(); + // Add dashlet to grid with its position and size + this.oGrid.AddDashlet(sDashletHtml, { + x: iPosX, + y: iPosY, + w: iWidth, + h: iHeight, + autoPosition: false + }); + }); + } } // Update last saved state diff --git a/sources/Application/Dashboard/Controller/DashboardController.php b/sources/Application/Dashboard/Controller/DashboardController.php index 55b5e1de6..ad6b0253e 100644 --- a/sources/Application/Dashboard/Controller/DashboardController.php +++ b/sources/Application/Dashboard/Controller/DashboardController.php @@ -18,7 +18,10 @@ use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory; use Combodo\iTop\Application\WebPage\AjaxPage; use Combodo\iTop\Application\WebPage\DownloadPage; use Combodo\iTop\Application\WebPage\JsonPage; +use Combodo\iTop\DesignDocument; +use Combodo\iTop\DesignElement; use Combodo\iTop\Forms\Block\FormBlockService; +use Combodo\iTop\PropertyType\PropertyTypeDesign; use Combodo\iTop\PropertyType\Serializer\XMLSerializer; use Combodo\iTop\Service\ServiceLocator\ServiceLocator; use DBObjectSearch; @@ -163,7 +166,7 @@ class DashboardController extends Controller throw new SecurityException('Invalid dashboard file !'); } - if (!appUserPreferences::GetPref('display_original_dashboard_'.$sDashboardId, false)) { + if (!filter_var(appUserPreferences::GetPref('display_original_dashboard_'.$sDashboardId, false), FILTER_VALIDATE_BOOLEAN)) { // Search for an eventual user defined dashboard $oUDSearch = new DBObjectSearch('UserDashboard'); $oUDSearch->AddCondition('user_id', UserRights::GetUserId(), '='); @@ -192,6 +195,85 @@ class DashboardController extends Controller return $oPage; } + public function OperationLoad() + { + $sDashboardId = utils::ReadParam('id', '', false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA); + $bIsCustom = utils::ReadParam('is_custom', 'false', false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA) === 'true'; + $sDashboardFile = APPROOT.utils::ReadParam('file', '', false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA); + + $sDashboardFileSanitized = utils::RealPath($sDashboardFile, APPROOT); + if (false === $sDashboardFileSanitized) { + throw new SecurityException('Invalid dashboard file !'); + } + + $sStatus = 'error'; + $sMessage = 'Unknown error'; + $aData = []; + + try { + if ($bIsCustom) { + // Search for an eventual user defined dashboard + $oUDSearch = new DBObjectSearch('UserDashboard'); + $oUDSearch->AddCondition('user_id', UserRights::GetUserId(), '='); + $oUDSearch->AddCondition('menu_code', $sDashboardId, '='); + $oUDSet = new DBObjectSet($oUDSearch); + if ($oUDSet->Count() > 0) { + // Assuming there is at most one couple {user, menu}! + $oUserDashboard = $oUDSet->Fetch(); + $sDashboardDefinition = $oUserDashboard->Get('contents'); + } else { + $sDashboardDefinition = @file_get_contents($sDashboardFileSanitized); + } + } + else { + $sDashboardDefinition = @file_get_contents($sDashboardFileSanitized); + } + + // TODO 3.3 If the dashboard definition is previous schema, we need to convert it to the new one + + + $oDoc = new PropertyTypeDesign(); + $oDoc->loadXML($sDashboardDefinition); + + $oMainNode = $oDoc->getElementsByTagName('dashboard')->item(0); + + $aData = $this->oXMLSerializer->Deserialize($oMainNode, 'DashboardGrid', 'Dashboard'); + + // TODO 3.3 Re-render every dashlet to have their latest representation + // Let's render every dashlet to prevent frontend from having to do it for each in individual ajax call +// if (array_key_exists('pos_dashlets', $aData)) { +// foreach ($aData['pos_dashlets'] as $sDashletId => $sPosValues) { +// if(array_key_exists('dashlet', $sPosValues)) { +// $sDashletClass = $sPosValues['dashlet']['type']; +// $aValues = $sPosValues['dashlet']['properties']; +// +// $oDashlet = DashletFactory::GetInstance()->CreateDashlet($sDashletClass, $sDashletId); +// $oDashlet->FromModelData($aValues); +// +// +// } +// } +// } + + + $sStatus = 'ok'; + $sMessage = 'Dashboard loaded'; + } catch (Exception $e) { + IssueLog::Exception($e->getMessage(), $e); + $sStatus = 'error'; + $sMessage = $e->getMessage(); + } + + $oPage = new JsonPage(); + $oPage->SetData([ + 'status' => $sStatus, + 'message' => $sMessage, + 'data' => $aData + ]); + $oPage->SetOutputDataOnly(true); + return $oPage; + } + public function OperationImport() { $oPage = new JsonPage(); diff --git a/sources/Application/Dashboard/Dashboard.php b/sources/Application/Dashboard/Dashboard.php index 492274878..b8b163663 100644 --- a/sources/Application/Dashboard/Dashboard.php +++ b/sources/Application/Dashboard/Dashboard.php @@ -364,6 +364,8 @@ abstract class Dashboard } $oDashboard = $oLayout->Render($oPage, $aDashlets, $bEditMode, $aExtraParams); + $oDashboard->SetFile(utils::LocalPath($this->GetDefinitionFile())); + $oPage->AddUiBlock($oDashboard); $bFromDashboardPage = isset($aExtraParams['from_dashboard_page']) ? isset($aExtraParams['from_dashboard_page']) : false; diff --git a/sources/Application/Dashboard/RuntimeDashboard.php b/sources/Application/Dashboard/RuntimeDashboard.php index 1279ad9ce..b777093ad 100644 --- a/sources/Application/Dashboard/RuntimeDashboard.php +++ b/sources/Application/Dashboard/RuntimeDashboard.php @@ -245,6 +245,10 @@ class RuntimeDashboard extends Dashboard $oDashboard = parent::Render($oPage, $bEditMode, $aRenderParams); + if($this->HasCustomDashboard() && !filter_var(appUserPreferences::GetPref('display_original_dashboard_'.$this->GetId(), false), FILTER_VALIDATE_BOOLEAN)) { + $oDashboard->SetIsCustom(true); + } + if (isset($aExtraParams['query_params']['this->object()'])) { /** @var \DBObject $oObj */ $oObj = $aExtraParams['query_params']['this->object()']; @@ -321,15 +325,12 @@ EOF $sSwitchToStandard = Dict::S('UI:Toggle:SwitchToStandardDashboard'); $sSwitchToCustom = Dict::S('UI:Toggle:SwitchToCustomDashboard'); - $bStandardSelected = appUserPreferences::GetPref('display_original_dashboard_'.$sId, false); + $bStandardSelected = filter_var(appUserPreferences::GetPref('display_original_dashboard_'.$sId, false), FILTER_VALIDATE_BOOLEAN); - $sSelectorHtml = '
'; - $sSelectorHtml .= ''; + $sSelectorHtml = '
'; + $sSelectorHtml .= ''; $sSelectorHtml .= '
'; - $sFile = addslashes($this->GetDefinitionFile()); - $sReloadURL = json_encode($this->GetReloadURL()); - $bFromDashboardPage = isset($aAjaxParams['from_dashboard_page']) ? isset($aAjaxParams['from_dashboard_page']) : false; if ($bFromDashboardPage) { if ($oPage instanceof iTopWebPage) { @@ -340,29 +341,6 @@ EOF $oToolbar = $oDashboard->GetToolbar(); $oToolbar->AddHtml($sSelectorHtml); } - - $oPage->add_script( - <<oTitleInput = $this->MakeTitleInput(); $this->oRefreshInput = $this->MakeRefreshInput(); $this->oButtonsToolbar = $this->MakeButtonsToolbar(); + $this->sFile = ''; } public function MakeTitleInput() @@ -192,4 +197,28 @@ class DashboardLayout extends UIBlock { return $this->oButtonsToolbar; } + + public function SetFile(string $sFile) + { + $this->sFile = $sFile; + + return $this; + } + + public function GetFile() + { + return $this->sFile; + } + + public function IsCustom(): bool + { + return $this->bIsCustom; + } + + public function SetIsCustom(bool $bIsCustom) + { + $this->bIsCustom = $bIsCustom; + + return $this; + } } diff --git a/templates/base/layouts/dashboard/layout.html.twig b/templates/base/layouts/dashboard/layout.html.twig index e871da38f..35edc5959 100644 --- a/templates/base/layouts/dashboard/layout.html.twig +++ b/templates/base/layouts/dashboard/layout.html.twig @@ -8,10 +8,16 @@ {{ render_block(oUIBlock.GetToolbar(), {aPage: aPage}) }}
{% endif %} - +
- Editing{{ render_block(oUIBlock.GetTitleInput(), {aPage: aPage}) }}{{ render_block(oUIBlock.GetRefreshInput(), {aPage: aPage}) }} + Editing {{ render_block(oUIBlock.GetTitleInput(), {aPage: aPage}) }}{{ render_block(oUIBlock.GetRefreshInput(), {aPage: aPage}) }}
{{ render_block(oUIBlock.GetButtonsToolbar(), {aPage: aPage}) }}