From 77a820105fda08118a5eb95ded98dd5bb18422e3 Mon Sep 17 00:00:00 2001 From: Timmy38 <101416770+Timmy38@users.noreply.github.com> Date: Thu, 21 May 2026 09:48:33 +0200 Subject: [PATCH] =?UTF-8?q?N=C2=B09167=20Use=20ExtensionDetails=20UIBlocks?= =?UTF-8?q?=20instead=20of=20table=20(#910)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layout/extension/_extension-details.scss | 13 +- .../DataFeatureRemovalController.php | 162 ++++++++++-------- .../DataFeatureRemoverExtensionService.php | 18 +- .../templates/AnalysisResult.html.twig | 19 +- .../templates/Features.html.twig | 18 +- .../cs.dictionary.itop.extensions-details.php | 1 + .../da.dictionary.itop.extensions-details.php | 1 + .../de.dictionary.itop.extensions-details.php | 1 + .../en.dictionary.itop.extensions-details.php | 1 + ..._cr.dictionary.itop.extensions-details.php | 1 + .../fr.dictionary.itop.extensions-details.php | 1 + .../hu.dictionary.itop.extensions-details.php | 1 + .../it.dictionary.itop.extensions-details.php | 1 + .../ja.dictionary.itop.extensions-details.php | 1 + .../nl.dictionary.itop.extensions-details.php | 1 + .../pl.dictionary.itop.extensions-details.php | 1 + ..._br.dictionary.itop.extensions-details.php | 1 + .../ru.dictionary.itop.extensions-details.php | 1 + .../sk.dictionary.itop.extensions-details.php | 1 + .../tr.dictionary.itop.extensions-details.php | 1 + ..._cn.dictionary.itop.extensions-details.php | 1 + setup/extensionsmap.class.inc.php | 18 +- .../DryRemovalRuntimeEnvironment.php | 6 +- setup/itopextension.class.inc.php | 14 ++ .../Layout/Extension/ExtensionDetails.php | 44 +++-- .../ExtensionDetailsUIBlockFactory.php | 30 +++- .../components/input/input-toggler.html.twig | 3 +- .../input/input-toggler.ready.js.twig | 2 + .../Backoffice/RenderAllUiBlocks.php | 3 + 29 files changed, 237 insertions(+), 129 deletions(-) diff --git a/css/backoffice/layout/extension/_extension-details.scss b/css/backoffice/layout/extension/_extension-details.scss index 0c39e8ba6b..3fd5e51c92 100644 --- a/css/backoffice/layout/extension/_extension-details.scss +++ b/css/backoffice/layout/extension/_extension-details.scss @@ -62,4 +62,15 @@ $ibo-extension-details--actions--button--padding-x: $ibo-button--padding-x !defa .ibo-extension-details--actions:has(.toggler-install:not(:disabled)) .ibo-popover-menu--section a[data-resource-id="force_uninstall"] { display: none; -} \ No newline at end of file +} + +.ibo-extension-details .ibo-popover-menu ~ .ibo-button{ + visibility: hidden; +} +.ibo-extension-details .ibo-popover-menu:has(.ibo-popover-menu--item) ~ .ibo-button{ + visibility: visible; +} + +.ibo-extension-details .ibo-toggler--wrapper:has(.ibo-toggler.ibo-is-hidden){ + visibility: hidden; +} diff --git a/datamodels/2.x/combodo-data-feature-removal/src/Controller/DataFeatureRemovalController.php b/datamodels/2.x/combodo-data-feature-removal/src/Controller/DataFeatureRemovalController.php index debd2655f1..04b6c9c2a3 100644 --- a/datamodels/2.x/combodo-data-feature-removal/src/Controller/DataFeatureRemovalController.php +++ b/datamodels/2.x/combodo-data-feature-removal/src/Controller/DataFeatureRemovalController.php @@ -31,20 +31,23 @@ use utils; class DataFeatureRemovalController extends Controller { private array $aRemovedExtensionsForCheck = []; + private ?array $aExtensionsToCheck = null; + private bool $bForcedUninstallation = false; private array $aCountClassesToCleanup = []; private array $aAnalysisDataTable = []; private array $aDeletionExecutionSummary = []; private int $iCount = 0; + private int $iColumnCount = 2; public function OperationMain($sErrorMessage = null): void { $aParams = []; - $this->ReadRemovedExtensions(); $this->AddAnalyzeParams(); $aParams['sTransactionId'] = utils::GetNewTransactionId(); - $aParams['aExtensions'] = $this->GetExtensionsTableToSelect(); + $aParams['iColumnCount'] = $this->iColumnCount; + $aParams['aAvailableExtensions'] = $this->SplitArrayIntoColumns($this->GetAvailableExtensions(), $this->iColumnCount); $aParams['aAnalysisDataTable'] = $this->aAnalysisDataTable; $aParams['aClasses'] = array_keys($this->aCountClassesToCleanup); $aParams['DataFeatureRemovalErrorMessage'] = $sErrorMessage; @@ -76,12 +79,11 @@ class DataFeatureRemovalController extends Controller public function OperationAnalyze(): void { - $this->ReadRemovedExtensions(); + $iCount = $this->ReadExtensionsDiff(); $this->m_sOperation = 'Main'; - try { - if (count($this->aRemovedExtensionsForCheck) > 0) { + if ($iCount > 0) { $this->Analyze(); } $this->OperationMain(); @@ -93,7 +95,8 @@ class DataFeatureRemovalController extends Controller private function Analyze(): void { - $this->Compile($this->aRemovedExtensionsForCheck); + //TODO : Run data audit with added extension too, not just removed ones + $this->Compile($this->aExtensionsToCheck['to_be_removed']); $sSourceEnv = MetaModel::GetEnvironment(); $oSetupAudit = new SetupAudit($sSourceEnv); $aGetRemovedClasses = $oSetupAudit->RunDataAudit(); @@ -148,7 +151,8 @@ class DataFeatureRemovalController extends Controller $aParams['sTransactionId'] = utils::GetNewTransactionId(); $aParams['aClasses'] = $aGetRemovedClasses; - $aParams['aExtensions'] = $this->GetExtensionsTableDiff($aAddedExtensions, $aRemovedExtensions); + $aParams['iColumnCount'] = $this->iColumnCount; + $aParams['aAvailableExtensions'] = $this->SplitArrayIntoColumns($this->GetExtensionsDiff($aAddedExtensions, $aRemovedExtensions), $this->iColumnCount); new ContextTag(ContextTag::TAG_SETUP); $aParams['sLaunchSetupUrl'] = utils::GetAbsoluteUrlAppRoot().'setup/wizard.php'; @@ -261,72 +265,51 @@ class DataFeatureRemovalController extends Controller $this->OperationAnalysisResult(); } - private function GetExtensionsTableDiff(array $aAddedExtensions, array $aRemovedExtensions): array + private function GetAvailableExtensions(bool $bIncludePackageExtensions = false): array { - $aExtensions = []; - $aColumns = ['', 'Name', 'code', 'Badge' ]; - - foreach ($aAddedExtensions as $sAddedExtensionCode => $sAddedExtensionLabel) { - $aExtensions[] = [ - << -HTML, - $sAddedExtensionLabel, - $sAddedExtensionCode, - Dict::S('UI:Layout:ExtensionsDetails:BadgeToBeInstalled'), - ]; + $aExtensionsData = []; + if ($bIncludePackageExtensions) { + $aExtensionsRef = DataFeatureRemoverExtensionService::GetInstance()->GetExtensionMap()->GetAllExtensionsWithPreviouslyInstalled(); + } else { + $aExtensionsRef = DataFeatureRemoverExtensionService::GetInstance()->ReadItopExtensions(); } - foreach ($aRemovedExtensions as $sAddedExtensionCode => $sAddedExtensionLabel) { - $aExtensions[] = [ - << -HTML, - $sAddedExtensionLabel, - $sAddedExtensionCode, - Dict::S('UI:Layout:ExtensionsDetails:BadgeToBeUninstalled'), + + foreach ($aExtensionsRef as $oExtension) { + /** @var \iTopExtension $oExtension */ + $aExtensionsData[$oExtension->sCode] = [ + 'version' => $oExtension->sVersion, + 'label' => $oExtension->sLabel, + 'code' => $oExtension->sCode, + 'description' => $oExtension->sDescription, + 'source' => $oExtension->GetExtensionSourceLabel(), + 'installed' => $oExtension->bInstalled, + 'extra_flags' => [ + 'uninstallable' => $oExtension->CanBeUninstalled(), + 'remote' => $oExtension->IsRemote(), + 'missing' => $oExtension->bRemovedFromDisk, + ], + ]; } - return $this->GetTableData('Extensions', $aColumns, $aExtensions); + return $aExtensionsData; } - /** - * Get installed extensions from disk - * - * @return array structure for twig datatable - */ - private function GetExtensionsTableToSelect(): array + private function GetExtensionsDiff(array $aAddedExtensions, array $aRemovedExtensions): array { $aExtensions = []; - $aColumns = ['', 'Version', 'Name', 'Code']; - - foreach (DataFeatureRemoverExtensionService::GetInstance()->ReadItopExtensions() as $sCode => $oExtension) { - /** @var \iTopExtension $oExtension */ - - $sChecked = ''; - $sDisabledHtml = ''; - if ($oExtension->bRemovedFromDisk) { - $sDisabledHtml = 'disabled=""'; - $sChecked = 'checked'; - } elseif (in_array($sCode, $this->aRemovedExtensionsForCheck)) { - $sChecked = 'checked'; + foreach ($this->GetAvailableExtensions(true) as $sCode => $aExtension) { + $aExtension['extra_flags']['disabled'] = true; + if (isset($aAddedExtensions[$sCode])) { + $aExtension['extra_flags']['selected'] = true; + $aExtensions[$sCode] = $aExtension; + } elseif (isset($aRemovedExtensions[$sCode])) { + $aExtension['extra_flags']['selected'] = false; + $aExtensions[$sCode] = $aExtension; } - - $sLabel = $oExtension->sLabel; - $sVersion = $oExtension->sVersion; - $sIdEnable = "aExtensions[$sCode][enable]"; - - $aExtensions[] = [ - << -HTML, - $sVersion, - $sLabel, - $sCode, - ]; } - return $this->GetTableData('Extensions', $aColumns, $aExtensions); + return $aExtensions; } private function GetTableData(string $sTableName, array $aColumns, array $aData): array @@ -370,27 +353,56 @@ HTML, } /** - * @return void + * Read extensions selected from posted parameters + * @return int Number of extensions to be added or removed */ - public function ReadRemovedExtensions(): void + public function ReadExtensionsDiff(): int { - if (count($this->aRemovedExtensionsForCheck) > 0) { - return; + if (!is_null($this->aExtensionsToCheck)) { + return count($this->aExtensionsToCheck['to_be_installed']) + count($this->aExtensionsToCheck['to_be_removed']); } - $aSelectedExtensionsFromUI = utils::ReadPostedParam('aExtensions', []); - foreach ($aSelectedExtensionsFromUI as $sCode => $aData) { - $sValue = $aData['enable'] ?? 'off'; - if (($sValue) === 'on') { - $this->aRemovedExtensionsForCheck[] = $sCode; + $aAvailableExtensions = $this->GetAvailableExtensions(); + $aSelectedExtensionsFromUI = utils::ReadPostedParam('aSelectedExtensions', []); + $this->aExtensionsToCheck = [ + 'to_be_installed' => [], + 'to_be_removed' => [], + ]; + foreach ($aAvailableExtensions as $sCode => &$aExtensionData) { + if (!isset($aSelectedExtensionsFromUI[$sCode])) { + continue; } - } - // Add source removed to check - foreach (DataFeatureRemoverExtensionService::GetInstance()->ReadItopExtensions() as $sCode => $oExtension) { - if ($oExtension->bRemovedFromDisk) { - $this->aRemovedExtensionsForCheck[] = $sCode; + if ($aExtensionData['installed'] && $aSelectedExtensionsFromUI[$sCode] !== 'on') { + $aExtensionData['extra_flags']['selected'] = false; + $this->aExtensionsToCheck['to_be_removed'][] = $sCode; + if (!$aExtensionData['extra_flags']['uninstallable'] || $aExtensionData['extra_flags']['remote']) { + $this->bForcedUninstallation = true; + } + } elseif (!$aExtensionData['installed'] && $aSelectedExtensionsFromUI[$sCode] === 'on') { + $aExtensionData['extra_flags']['selected'] = true; + $this->aExtensionsToCheck['to_be_installed'][] = $sCode; } } + return count($this->aExtensionsToCheck['to_be_installed']) + count($this->aExtensionsToCheck['to_be_removed']); + } + + /** + * Divide an array into several sub arrays, distributing elements so that every sub array has an equal amount of elements + * @param mixed[] $aInput + * @param int $iColNumber + * + * @return array[] + */ + private function SplitArrayIntoColumns(array $aInput, int $iColNumber) + { + $aOutput = array_fill(0, $iColNumber, []); + $iIndex = 0; + foreach ($aInput as $mItem) { + //Split extensions in $iColNumber columns + $aOutput[$iIndex % $this->iColumnCount][] = $mItem; + $iIndex++; + } + return $aOutput; } } diff --git a/datamodels/2.x/combodo-data-feature-removal/src/Service/DataFeatureRemoverExtensionService.php b/datamodels/2.x/combodo-data-feature-removal/src/Service/DataFeatureRemoverExtensionService.php index 5dcdaa4169..e25c5a755a 100644 --- a/datamodels/2.x/combodo-data-feature-removal/src/Service/DataFeatureRemoverExtensionService.php +++ b/datamodels/2.x/combodo-data-feature-removal/src/Service/DataFeatureRemoverExtensionService.php @@ -14,7 +14,7 @@ use MetaModel; class DataFeatureRemoverExtensionService { private static DataFeatureRemoverExtensionService $oInstance; - + private ?iTopExtensionsMap $oMap = null; private array $aItopExtensions = []; private array $aIncludingExtensionsByModuleName = []; @@ -60,15 +60,25 @@ class DataFeatureRemoverExtensionService return $this->aIncludingExtensionsByModuleName[$sModuleName] ?? []; } + /** + * @return \iTopExtensionsMap + */ + public function GetExtensionMap(): iTopExtensionsMap + { + if (is_null($this->oMap)) { + $this->oMap = new iTopExtensionsMap(); + $this->oMap->LoadInstalledExtensionsFromDatabase(MetaModel::GetConfig()); + } + return $this->oMap; + } + /** * @return iTopExtension[] */ public function ReadItopExtensions(): array { if (count($this->aItopExtensions) === 0) { - $oExtensionsMap = new iTopExtensionsMap(); - $oExtensionsMap->LoadInstalledExtensionsFromDatabase(MetaModel::GetConfig()); - $this->aItopExtensions = $oExtensionsMap->GetAllExtensionsToDisplayInSetup(true); + $this->aItopExtensions = $this->GetExtensionMap()->GetAllExtensionsToDisplayInSetup(true); uasort($this->aItopExtensions, function (iTopExtension $oiTopExtension1, iTopExtension $oiTopExtension2) { return strcmp($oiTopExtension1->sLabel, $oiTopExtension2->sLabel); diff --git a/datamodels/2.x/combodo-data-feature-removal/templates/AnalysisResult.html.twig b/datamodels/2.x/combodo-data-feature-removal/templates/AnalysisResult.html.twig index e585003983..2ce1f409f1 100644 --- a/datamodels/2.x/combodo-data-feature-removal/templates/AnalysisResult.html.twig +++ b/datamodels/2.x/combodo-data-feature-removal/templates/AnalysisResult.html.twig @@ -4,10 +4,21 @@ {% UIPanel ForInformation { sTitle:'DataFeatureRemoval:Analysis:Title'|dict_s} %} - - {% UIFieldSet Standard {sLegend:'DataFeatureRemoval:Features:Title'|dict_s} %} - {% UIDataTable ForForm { sRef:'aExtensions', aColumns:aExtensions.Columns, aData:aExtensions.Data} %}{% EndUIDataTable %} - {% EndUIFieldSet %} + {% UIPanel Neutral { sTitle:'DataFeatureRemoval:Features:Title'|dict_s, sSubTitle: '' } %} + {% UIMultiColumn Standard {} %} + {% for iColumnIndex in 0..iColumnCount-1 %} + {% UIColumn Standard {} %} + {% for aExtension in aAvailableExtensions[iColumnIndex] %} + {% if aExtension['installed'] %} + {% UIExtensionDetails Installed { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %} + {% else %} + {% UIExtensionDetails NotInstalled { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %} + {% endif %} + {% endfor %} + {% EndUIColumn %} + {% endfor %} + {% EndUIMultiColumn %} + {% EndUIPanel %} {% if bDeletionNeeded %} {% UIFieldSet Standard {sLegend:'DataFeatureRemoval:DeletionPlan:Title'|dict_s} %} diff --git a/datamodels/2.x/combodo-data-feature-removal/templates/Features.html.twig b/datamodels/2.x/combodo-data-feature-removal/templates/Features.html.twig index 26c3f15a10..71b06b6b64 100644 --- a/datamodels/2.x/combodo-data-feature-removal/templates/Features.html.twig +++ b/datamodels/2.x/combodo-data-feature-removal/templates/Features.html.twig @@ -6,9 +6,21 @@ {% UIInput ForHidden {sName:'operation', sValue:'Analyze'} %} {% UIInput ForHidden {sName:'transaction_id', sValue:sTransactionId} %} - {% UIFieldSet Standard {sLegend:'DataFeatureRemoval:Features:Title'|dict_s} %} - {% UIDataTable ForForm { sRef:'aExtensions', aColumns:aExtensions.Columns, aData:aExtensions.Data} %}{% EndUIDataTable %} - {% EndUIFieldSet %} + {% UIPanel Neutral { sTitle:'DataFeatureRemoval:Features:Title'|dict_s, sSubTitle: '' } %} + {% UIMultiColumn Standard {} %} + {% for iColumnIndex in 0..iColumnCount-1 %} + {% UIColumn Standard {} %} + {% for aExtension in aAvailableExtensions[iColumnIndex] %} + {% if aExtension['installed'] %} + {% UIExtensionDetails Installed { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %} + {% else %} + {% UIExtensionDetails NotInstalled { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %} + {% endif %} + {% endfor %} + {% EndUIColumn %} + {% endfor %} + {% EndUIMultiColumn %} + {% EndUIPanel %} {% UIToolbar ForButton {} %} {% UIButton ForPrimaryAction {sLabel:'UI:Button:Analyze'|dict_s, sName:'btn_apply', sId:'btn_apply', bIsSubmit:true} %} diff --git a/dictionaries/ui/layouts/extensions-details/cs.dictionary.itop.extensions-details.php b/dictionaries/ui/layouts/extensions-details/cs.dictionary.itop.extensions-details.php index 61c54f50a5..2f38ac0a26 100644 --- a/dictionaries/ui/layouts/extensions-details/cs.dictionary.itop.extensions-details.php +++ b/dictionaries/ui/layouts/extensions-details/cs.dictionary.itop.extensions-details.php @@ -20,4 +20,5 @@ Dict::Add('CS CZ', 'Czech', 'Čeština', [ 'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~', 'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~', 'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~', + 'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~', ]); diff --git a/dictionaries/ui/layouts/extensions-details/da.dictionary.itop.extensions-details.php b/dictionaries/ui/layouts/extensions-details/da.dictionary.itop.extensions-details.php index 9891a10b7d..ad743ccc36 100644 --- a/dictionaries/ui/layouts/extensions-details/da.dictionary.itop.extensions-details.php +++ b/dictionaries/ui/layouts/extensions-details/da.dictionary.itop.extensions-details.php @@ -20,4 +20,5 @@ Dict::Add('DA DA', 'Danish', 'Dansk', [ 'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~', 'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~', 'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~', + 'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~', ]); diff --git a/dictionaries/ui/layouts/extensions-details/de.dictionary.itop.extensions-details.php b/dictionaries/ui/layouts/extensions-details/de.dictionary.itop.extensions-details.php index 8cca5ffa4c..505ce5e34e 100644 --- a/dictionaries/ui/layouts/extensions-details/de.dictionary.itop.extensions-details.php +++ b/dictionaries/ui/layouts/extensions-details/de.dictionary.itop.extensions-details.php @@ -20,4 +20,5 @@ Dict::Add('DE DE', 'German', 'Deutsch', [ 'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~', 'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~', 'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~', + 'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~', ]); diff --git a/dictionaries/ui/layouts/extensions-details/en.dictionary.itop.extensions-details.php b/dictionaries/ui/layouts/extensions-details/en.dictionary.itop.extensions-details.php index 955c5bae30..b8d1415ced 100644 --- a/dictionaries/ui/layouts/extensions-details/en.dictionary.itop.extensions-details.php +++ b/dictionaries/ui/layouts/extensions-details/en.dictionary.itop.extensions-details.php @@ -18,4 +18,5 @@ Dict::Add('EN US', 'English', 'English', [ 'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s', 'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations', 'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall', + 'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions', ]); diff --git a/dictionaries/ui/layouts/extensions-details/es_cr.dictionary.itop.extensions-details.php b/dictionaries/ui/layouts/extensions-details/es_cr.dictionary.itop.extensions-details.php index f85e8be400..ae18bb850c 100644 --- a/dictionaries/ui/layouts/extensions-details/es_cr.dictionary.itop.extensions-details.php +++ b/dictionaries/ui/layouts/extensions-details/es_cr.dictionary.itop.extensions-details.php @@ -20,4 +20,5 @@ Dict::Add('ES CR', 'Spanish', 'Español, Castellano', [ 'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~', 'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~', 'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~', + 'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~', ]); diff --git a/dictionaries/ui/layouts/extensions-details/fr.dictionary.itop.extensions-details.php b/dictionaries/ui/layouts/extensions-details/fr.dictionary.itop.extensions-details.php index ce8f70f125..5114c91c77 100644 --- a/dictionaries/ui/layouts/extensions-details/fr.dictionary.itop.extensions-details.php +++ b/dictionaries/ui/layouts/extensions-details/fr.dictionary.itop.extensions-details.php @@ -20,4 +20,5 @@ Dict::Add('FR FR', 'French', 'Français', [ 'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'À propos de %1$s', 'UI:Layout:ExtensionsDetails:MenuAbout' => 'Plus d\'informations', 'UI:Layout:ExtensionsDetails:MenuForce' => 'Forcer la désinstallation', + 'UI:Layout:ExtensionsDetails:MoreActions' => 'Plus d\'actions', ]); diff --git a/dictionaries/ui/layouts/extensions-details/hu.dictionary.itop.extensions-details.php b/dictionaries/ui/layouts/extensions-details/hu.dictionary.itop.extensions-details.php index 69d6dcfe2a..cded3a4641 100644 --- a/dictionaries/ui/layouts/extensions-details/hu.dictionary.itop.extensions-details.php +++ b/dictionaries/ui/layouts/extensions-details/hu.dictionary.itop.extensions-details.php @@ -20,4 +20,5 @@ Dict::Add('HU HU', 'Hungarian', 'Magyar', [ 'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~', 'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~', 'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~', + 'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~', ]); diff --git a/dictionaries/ui/layouts/extensions-details/it.dictionary.itop.extensions-details.php b/dictionaries/ui/layouts/extensions-details/it.dictionary.itop.extensions-details.php index 4755058230..424a0b13f9 100644 --- a/dictionaries/ui/layouts/extensions-details/it.dictionary.itop.extensions-details.php +++ b/dictionaries/ui/layouts/extensions-details/it.dictionary.itop.extensions-details.php @@ -20,4 +20,5 @@ Dict::Add('IT IT', 'Italian', 'Italiano', [ 'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~', 'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~', 'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~', + 'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~', ]); diff --git a/dictionaries/ui/layouts/extensions-details/ja.dictionary.itop.extensions-details.php b/dictionaries/ui/layouts/extensions-details/ja.dictionary.itop.extensions-details.php index 0d07ae7fa9..96703e5832 100644 --- a/dictionaries/ui/layouts/extensions-details/ja.dictionary.itop.extensions-details.php +++ b/dictionaries/ui/layouts/extensions-details/ja.dictionary.itop.extensions-details.php @@ -20,4 +20,5 @@ Dict::Add('JA JP', 'Japanese', '日本語', [ 'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~', 'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~', 'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~', + 'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~', ]); diff --git a/dictionaries/ui/layouts/extensions-details/nl.dictionary.itop.extensions-details.php b/dictionaries/ui/layouts/extensions-details/nl.dictionary.itop.extensions-details.php index c86e35a500..1774a4dca1 100644 --- a/dictionaries/ui/layouts/extensions-details/nl.dictionary.itop.extensions-details.php +++ b/dictionaries/ui/layouts/extensions-details/nl.dictionary.itop.extensions-details.php @@ -20,4 +20,5 @@ Dict::Add('NL NL', 'Dutch', 'Nederlands', [ 'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~', 'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~', 'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~', + 'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~', ]); diff --git a/dictionaries/ui/layouts/extensions-details/pl.dictionary.itop.extensions-details.php b/dictionaries/ui/layouts/extensions-details/pl.dictionary.itop.extensions-details.php index 1fd145907d..48628bd08e 100644 --- a/dictionaries/ui/layouts/extensions-details/pl.dictionary.itop.extensions-details.php +++ b/dictionaries/ui/layouts/extensions-details/pl.dictionary.itop.extensions-details.php @@ -20,4 +20,5 @@ Dict::Add('PL PL', 'Polish', 'Polski', [ 'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~', 'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~', 'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~', + 'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~', ]); diff --git a/dictionaries/ui/layouts/extensions-details/pt_br.dictionary.itop.extensions-details.php b/dictionaries/ui/layouts/extensions-details/pt_br.dictionary.itop.extensions-details.php index bf3d3b9a98..f469ddfdfc 100644 --- a/dictionaries/ui/layouts/extensions-details/pt_br.dictionary.itop.extensions-details.php +++ b/dictionaries/ui/layouts/extensions-details/pt_br.dictionary.itop.extensions-details.php @@ -20,4 +20,5 @@ Dict::Add('PT BR', 'Brazilian', 'Brazilian', [ 'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~', 'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~', 'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~', + 'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~', ]); diff --git a/dictionaries/ui/layouts/extensions-details/ru.dictionary.itop.extensions-details.php b/dictionaries/ui/layouts/extensions-details/ru.dictionary.itop.extensions-details.php index 57fdc12b09..9b5967a04d 100644 --- a/dictionaries/ui/layouts/extensions-details/ru.dictionary.itop.extensions-details.php +++ b/dictionaries/ui/layouts/extensions-details/ru.dictionary.itop.extensions-details.php @@ -20,4 +20,5 @@ Dict::Add('RU RU', 'Russian', 'Русский', [ 'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~', 'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~', 'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~', + 'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~', ]); diff --git a/dictionaries/ui/layouts/extensions-details/sk.dictionary.itop.extensions-details.php b/dictionaries/ui/layouts/extensions-details/sk.dictionary.itop.extensions-details.php index 16aba76ca6..3aab38ee98 100644 --- a/dictionaries/ui/layouts/extensions-details/sk.dictionary.itop.extensions-details.php +++ b/dictionaries/ui/layouts/extensions-details/sk.dictionary.itop.extensions-details.php @@ -20,4 +20,5 @@ Dict::Add('SK SK', 'Slovak', 'Slovenčina', [ 'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~', 'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~', 'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~', + 'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~', ]); diff --git a/dictionaries/ui/layouts/extensions-details/tr.dictionary.itop.extensions-details.php b/dictionaries/ui/layouts/extensions-details/tr.dictionary.itop.extensions-details.php index 0a8f85fe62..45ca6000a3 100644 --- a/dictionaries/ui/layouts/extensions-details/tr.dictionary.itop.extensions-details.php +++ b/dictionaries/ui/layouts/extensions-details/tr.dictionary.itop.extensions-details.php @@ -20,4 +20,5 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', [ 'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~', 'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~', 'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~', + 'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~', ]); diff --git a/dictionaries/ui/layouts/extensions-details/zh_cn.dictionary.itop.extensions-details.php b/dictionaries/ui/layouts/extensions-details/zh_cn.dictionary.itop.extensions-details.php index 1a8fe44f56..afce37f0ac 100644 --- a/dictionaries/ui/layouts/extensions-details/zh_cn.dictionary.itop.extensions-details.php +++ b/dictionaries/ui/layouts/extensions-details/zh_cn.dictionary.itop.extensions-details.php @@ -20,4 +20,5 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [ 'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~', 'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~', 'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~', + 'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~', ]); diff --git a/setup/extensionsmap.class.inc.php b/setup/extensionsmap.class.inc.php index 11dc6e063a..233c8e3bd9 100644 --- a/setup/extensionsmap.class.inc.php +++ b/setup/extensionsmap.class.inc.php @@ -486,7 +486,7 @@ class iTopExtensionsMap 'default' => true, // by default offer to install all modules 'modules' => $oExtension->aModules, 'mandatory' => $oExtension->bMandatory, - 'source_label' => $this->GetExtensionSourceLabel($oExtension->sSource), + 'source_label' => $oExtension->GetExtensionSourceLabel(), 'uninstallable' => $oExtension->CanBeUninstalled(), 'missing' => $oExtension->bRemovedFromDisk, 'version' => $oExtension->sVersion, @@ -496,22 +496,6 @@ class iTopExtensionsMap return $aRes; } - protected function GetExtensionSourceLabel($sSource) - { - $sResult = ''; - switch ($sSource) { - case iTopExtension::SOURCE_MANUAL: - $sResult = 'Local extensions folder'; - break; - - case iTopExtension::SOURCE_REMOTE: - $sResult = (ITOP_APPLICATION == 'iTop') ? 'iTop Hub' : 'ITSM Designer'; - break; - - } - return $sResult; - } - /** * Mark the given extension as chosen * @param string $sExtensionCode The code of the extension (code without version number) diff --git a/setup/feature_removal/DryRemovalRuntimeEnvironment.php b/setup/feature_removal/DryRemovalRuntimeEnvironment.php index ed86e87e2b..4cb8d1d42f 100644 --- a/setup/feature_removal/DryRemovalRuntimeEnvironment.php +++ b/setup/feature_removal/DryRemovalRuntimeEnvironment.php @@ -8,7 +8,7 @@ use SetupUtils; class DryRemovalRuntimeEnvironment extends RunTimeEnvironment { - protected array $aExtensionsByCode; + protected array $aExtensionsToRemoveByCode; /** * Toolset for building a run-time environment @@ -18,7 +18,7 @@ class DryRemovalRuntimeEnvironment extends RunTimeEnvironment public function __construct($sSourceEnv = 'production', array $aExtensionCodesToRemove = []) { parent::__construct($sSourceEnv, false); - $this->aExtensionsByCode = $aExtensionCodesToRemove; + $this->aExtensionsToRemoveByCode = $aExtensionCodesToRemove; $this->Prepare($sSourceEnv, $this->sBuildEnv); } @@ -34,7 +34,7 @@ class DryRemovalRuntimeEnvironment extends RunTimeEnvironment SetupUtils::copydir(APPROOT."/data/$sSourceEnv-modules", APPROOT."/data/$sBuildEnv-modules"); SetupUtils::copydir(APPROOT."/conf/$sSourceEnv", APPROOT."/conf/$sBuildEnv"); - $this->DeclareExtensionAsRemoved($this->aExtensionsByCode); + $this->DeclareExtensionAsRemoved($this->aExtensionsToRemoveByCode); } private function DeclareExtensionAsRemoved(array $aExtensionCodes): void diff --git a/setup/itopextension.class.inc.php b/setup/itopextension.class.inc.php index b40a64d74a..e2d7302c0b 100644 --- a/setup/itopextension.class.inc.php +++ b/setup/itopextension.class.inc.php @@ -169,4 +169,18 @@ class iTopExtension return json_encode($this->__serialize(), JSON_PRETTY_PRINT); } + public function GetExtensionSourceLabel(): string + { + return match ($this->sSource) { + self::SOURCE_MANUAL => 'Local extensions folder', + self::SOURCE_REMOTE => (ITOP_APPLICATION == 'iTop') ? 'iTop Hub' : 'ITSM Designer', + self::SOURCE_WIZARD => 'iTop package', + default => '', + }; + } + + public function IsRemote(): string + { + return $this->sSource === self::SOURCE_REMOTE; + } } diff --git a/sources/Application/UI/Base/Layout/Extension/ExtensionDetails.php b/sources/Application/UI/Base/Layout/Extension/ExtensionDetails.php index 0d03ebd6c7..bf53a364c9 100644 --- a/sources/Application/UI/Base/Layout/Extension/ExtensionDetails.php +++ b/sources/Application/UI/Base/Layout/Extension/ExtensionDetails.php @@ -33,7 +33,7 @@ class ExtensionDetails extends UIContentBlock $this->sCode = $sCode; $this->sLabel = $sLabel; $this->sDescription = $sDescription; - $this->aMetaData = $aMetaData; + $this->aMetaData = array_filter($aMetaData); $this->aBadges = $aBadges; $this->sAbout = $sAbout; $this->InitializeToggler(); @@ -105,7 +105,7 @@ class ExtensionDetails extends UIContentBlock */ public function SetMetaData(array $aMetaData): static { - $this->aMetaData = $aMetaData; + $this->aMetaData = array_filter($aMetaData); return $this; } @@ -179,32 +179,37 @@ class ExtensionDetails extends UIContentBlock protected function InitializeToggler() { + $sName = 'aSelectedExtensions['.$this->GetCode().']'; $this->oToggler = new Toggler(); - $this->oToggler->SetName('ExtensionToggler'); + $this->oToggler->SetName($sName); $this->oToggler->AddCSSClass('toggler-install'); } protected function InitializePopoverMenu() { - $sModalLabel = Dict::Format('UI:Layout:ExtensionsDetails:MenuAboutTitle', $this->sLabel); - $sModalText = $this->sAbout; - $oModifyButton = new JSButtonItem( - 'extension_details', - Dict::S('UI:Layout:ExtensionsDetails:MenuAbout'), - <<oPopoverMenu = new PopoverMenu(); + $oPopoverOpenButton = ButtonUIBlockFactory::MakeIconAction('fas fa-ellipsis-v', Dict::S('UI:Layout:ExtensionsDetails:MoreActions')); + $this->oPopoverMenu->SetTogglerFromBlock($oPopoverOpenButton); + $this->oMoreActions = new UIContentBlock(); + $this->oMoreActions->AddSubBlock($this->oPopoverMenu); + $this->oMoreActions->AddSubBlock($oPopoverOpenButton); + + if (mb_strlen($this->sAbout) > 0) { + $sModalLabel = Dict::Format('UI:Layout:ExtensionsDetails:MenuAboutTitle', $this->sLabel); + $sModalText = $this->sAbout; + $oModifyButton = new JSButtonItem( + 'extension_details', + Dict::S('UI:Layout:ExtensionsDetails:MenuAbout'), + <<oPopoverMenu = new PopoverMenu(); - $this->oPopoverMenu->AddItem('more-actions', PopoverMenuItemFactory::MakeFromApplicationPopupMenuItem($oModifyButton)); - $oPopoverOpenButton = ButtonUIBlockFactory::MakeIconAction('fas fa-ellipsis-v', 'Show more actions'); - $this->oPopoverMenu->SetTogglerFromBlock($oPopoverOpenButton); - $this->oMoreActions = new UIContentBlock(); - $this->oMoreActions->AddSubBlock($this->oPopoverMenu); - $this->oMoreActions->AddSubBlock($oPopoverOpenButton); + ); + $this->oPopoverMenu->AddItem('more-details', PopoverMenuItemFactory::MakeFromApplicationPopupMenuItem($oModifyButton)); + } + } public function AllowForceUninstall() @@ -213,10 +218,11 @@ JS, 'force_uninstall', Dict::S('UI:Layout:ExtensionsDetails:MenuForce'), <<oPopoverMenu->AddItem('more-actions', PopoverMenuItemFactory::MakeFromApplicationPopupMenuItem($oForceUninstallButton)); + $this->oPopoverMenu->AddItem('force-uninstall', PopoverMenuItemFactory::MakeFromApplicationPopupMenuItem($oForceUninstallButton)); } } diff --git a/sources/Application/UI/Base/Layout/Extension/ExtensionDetailsUIBlockFactory.php b/sources/Application/UI/Base/Layout/Extension/ExtensionDetailsUIBlockFactory.php index 7ec0db7b26..7ab01cd07a 100644 --- a/sources/Application/UI/Base/Layout/Extension/ExtensionDetailsUIBlockFactory.php +++ b/sources/Application/UI/Base/Layout/Extension/ExtensionDetailsUIBlockFactory.php @@ -18,6 +18,9 @@ class ExtensionDetailsUIBlockFactory extends AbstractUIBlockFactory $aBadges = []; $bUninstallable = $aExtraFlags['uninstallable'] ?? true; $bMissingFromDisk = $aExtraFlags['missing'] ?? false; + $bSelected = $aExtraFlags['selected'] ?? true; + $bDisabled = $aExtraFlags['disabled'] ?? false; + $bRemote = $aExtraFlags['remote'] ?? false; self::AddExtraBadges($aBadges, $bUninstallable, $bMissingFromDisk); $oBadgeInstalled = BadgeUIBlockFactory::MakeGreen(Dict::S('UI:Layout:ExtensionsDetails:BadgeInstalled')); $oBadgeInstalled->AddCSSClass('checked'); @@ -28,10 +31,22 @@ class ExtensionDetailsUIBlockFactory extends AbstractUIBlockFactory $oExtensionDetails = new ExtensionDetails($sCode, $sLabel, $sDescription, $aMetaData, $aBadges, $sAbout); $oExtensionDetails->GetToggler()->SetIsToggled(true); - if (!$bUninstallable) { + if ($bMissingFromDisk) { + $oExtensionDetails->GetToggler()->SetIsToggled(false); + $oExtensionDetails->GetToggler()->SetIsDisabled(true); + } elseif (!$bUninstallable || $bRemote) { $oExtensionDetails->AllowForceUninstall(); $oExtensionDetails->GetToggler()->SetIsDisabled(true); } + + if (!$bSelected) { + $oExtensionDetails->GetToggler()->SetIsToggled(false); + } + if ($bDisabled) { + $oExtensionDetails->GetToggler()->SetIsDisabled(true); + $oExtensionDetails->GetToggler()->AddCSSClass('ibo-is-hidden'); + } + return $oExtensionDetails; } @@ -39,6 +54,8 @@ class ExtensionDetailsUIBlockFactory extends AbstractUIBlockFactory { $aBadges = []; $bUninstallable = $aExtraFlags['uninstallable'] ?? true; + $bSelected = $aExtraFlags['selected'] ?? false; + $bDisabled = $aExtraFlags['disabled'] ?? false; self::AddExtraBadges($aBadges, $bUninstallable, false); $oBadgeInstalled = BadgeUIBlockFactory::MakeGrey(Dict::S('UI:Layout:ExtensionsDetails:BadgeNotInstalled')); $oBadgeInstalled->AddCSSClass('unchecked'); @@ -46,8 +63,17 @@ class ExtensionDetailsUIBlockFactory extends AbstractUIBlockFactory $oBadgeToBeUninstalled = BadgeUIBlockFactory::MakeCyan(Dict::S('UI:Layout:ExtensionsDetails:BadgeToBeInstalled')); $oBadgeToBeUninstalled->AddCSSClass('checked'); $aBadges[] = $oBadgeToBeUninstalled; + $oExtensionDetails = new ExtensionDetails($sCode, $sLabel, $sDescription, $aMetaData, $aBadges, $sAbout); - return new ExtensionDetails($sCode, $sLabel, $sDescription, $aMetaData, $aBadges, $sAbout); + if ($bSelected) { + $oExtensionDetails->GetToggler()->SetIsToggled(true); + } + if ($bDisabled) { + $oExtensionDetails->GetToggler()->SetIsDisabled(true); + $oExtensionDetails->GetToggler()->AddCSSClass('ibo-is-hidden'); + } + + return $oExtensionDetails; } private static function AddExtraBadges(array &$aBadges, bool $bUninstallable, bool $bMissingFromDisk) diff --git a/templates/base/components/input/input-toggler.html.twig b/templates/base/components/input/input-toggler.html.twig index f9bd74c6b5..1674d06cb9 100644 --- a/templates/base/components/input/input-toggler.html.twig +++ b/templates/base/components/input/input-toggler.html.twig @@ -1,7 +1,8 @@ {% extends "base/components/input/layout.html.twig" %} {% block iboInput %} - {{ parent() }} + {{ parent() }} + {% endblock %} diff --git a/templates/base/components/input/input-toggler.ready.js.twig b/templates/base/components/input/input-toggler.ready.js.twig index ec3e102fbc..703c0c5ad9 100644 --- a/templates/base/components/input/input-toggler.ready.js.twig +++ b/templates/base/components/input/input-toggler.ready.js.twig @@ -1,8 +1,10 @@ $('#{{ oUIBlock.GetId() }}').parent().on('click', function() { let oInput = $(this).find('.ibo-toggler'); + let oHiddenInput = $(this).find('.ibo-toggler--hidden'); if (!oInput.prop('disabled')) { oInput.prop('checked', !oInput.prop('checked')); + oHiddenInput.val(oInput.prop('checked') ? 'on' : 'off'); oInput.trigger('change'); } }); \ No newline at end of file diff --git a/tests/manual-visual-tests/Backoffice/RenderAllUiBlocks.php b/tests/manual-visual-tests/Backoffice/RenderAllUiBlocks.php index 2aac9ef2f0..a1bb135d05 100644 --- a/tests/manual-visual-tests/Backoffice/RenderAllUiBlocks.php +++ b/tests/manual-visual-tests/Backoffice/RenderAllUiBlocks.php @@ -639,6 +639,9 @@ $oPage->AddUiBlock($oMultiCol); $oExtensionDetailInstalledFromFactory = ExtensionDetailsUIBlockFactory::MakeInstalled('itop-sample', 'My extension v2', 'This is for test only', ['v1.1.1', 'Designer', '12/12/2012'], ['uninstallable' => false,'missing' => true]); $oColumnLeft->AddSubBlock($oExtensionDetailInstalledFromFactory); +$oExtensionDetailInstalledFromFactory = ExtensionDetailsUIBlockFactory::MakeInstalled('itop-not-uninstallable', 'You cannot uninstall me', 'Click force uninstall to uninstall me', ['v9.9.9', 'Void', '12/12/2012'], ['uninstallable' => false,'missing' => false]); +$oColumnLeft->AddSubBlock($oExtensionDetailInstalledFromFactory); + $oExtensionDetailInstalledWithLongTitle = ExtensionDetailsUIBlockFactory::MakeNotInstalled('itop-sample', 'My extension with a very long title', 'This is for test only', ['v1.1.1', 'Designer', '12/12/2012'], ['uninstallable' => false]); $oColumnRight->AddSubBlock($oExtensionDetailInstalledWithLongTitle); $oPage->add('
');