diff --git a/core/config.class.inc.php b/core/config.class.inc.php index fe3a11a1c..89e679c98 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -946,6 +946,14 @@ class Config 'source_of_value' => '', 'show_in_conf_sample' => false, ], + 'lifecycle.transitions_sort_type' => [ + 'type' => 'string', + 'description' => 'How transitions will be sorted in the GUI. Possible values are "xml", "alphabetic", "fixed" or "relative"', + 'default' => DBObject::DEFAULT_TRANSITIONS_SORT_TYPE, + 'value' => DBObject::DEFAULT_TRANSITIONS_SORT_TYPE, + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ], 'url_validation_pattern' => [ 'type' => 'string', 'description' => 'Regular expression to validate/detect the format of an URL (URL attributes and Wiki formatting for Text attributes)', diff --git a/core/dbobject.class.php b/core/dbobject.class.php index 2d3297e99..40a19a363 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -64,6 +64,32 @@ require_once('mutex.class.inc.php'); */ abstract class DBObject implements iDisplay { + /** + * @var string For sorting based on the XML order (like in iTop 3.0 and older) + * @since 3.1.0 N°1345 + */ + public const ENUM_TRANSITIONS_SORT_TYPE_XML = 'xml'; + /** + * @var string For sorting based on the transitions labels + * @since 3.1.0 N°1345 + */ + public const ENUM_TRANSITIONS_SORT_TYPE_ALPHABETICAL = 'alphabetical'; + /** + * @var string For sorting based on the arrival states rank, ascending sort + * @since 3.1.0 N°1345 + */ + public const ENUM_TRANSITIONS_SORT_TYPE_FIXED = 'fixed'; + /** + * @var string For sorting based on the arrival states rank but depending on the current state (first transitions to states with higher ranks, then transitions to states with lower ranks) + * @since 3.1.0 N°1345 + */ + public const ENUM_TRANSITIONS_SORT_TYPE_RELATIVE = 'relative'; + /** + * @var string Default sort type of the transitions + * @since 3.1.0 N°1345 + */ + public const DEFAULT_TRANSITIONS_SORT_TYPE = self::ENUM_TRANSITIONS_SORT_TYPE_RELATIVE; + private static $m_aMemoryObjectsByClass = array(); /** @var array class => array of ('table' => array of (array of )) */ @@ -3939,11 +3965,83 @@ abstract class DBObject implements iDisplay public function EnumTransitions() { $sClass = get_class($this); - if (!MetaModel::HasLifecycle($sClass)) return array(); + if (!MetaModel::HasLifecycle($sClass)) { + return []; + } $sStateAttCode = MetaModel::GetStateAttributeCode($sClass); $sState = $this->Get($sStateAttCode); - return MetaModel::EnumTransitions($sClass, $sState); + $aTransitions = MetaModel::EnumTransitions($sClass, $sState); + + if (count($aTransitions) === 0) { + return $aTransitions; + } + + // Try to sort transitions depending on the configuration + $sSortType = utils::GetConfig()->Get('lifecycle.transitions_sort_type'); + switch ($sSortType) { + case static::ENUM_TRANSITIONS_SORT_TYPE_XML: + $aSortedTransitions = $aTransitions; + break; + + case static::ENUM_TRANSITIONS_SORT_TYPE_ALPHABETICAL: + $aSortedTransitions = $aTransitions; + $aStimuli = MetaModel::EnumStimuli($sClass); + + // Sort $aSortedTransitions based on labels from $aStimuli + uksort($aSortedTransitions, function($sKey1, $sKey2) use ($aStimuli) { + // If any transition is not in $aStimuli, put it at the end even though it's a weird situation + if ((false === isset($aStimuli[$sKey1])) || (false === isset($aStimuli[$sKey2]))) { + return 1; + } + return $aStimuli[$sKey1]->GetLabel() > $aStimuli[$sKey2]->GetLabel() ? 1 : -1; + }); + break; + + case static::ENUM_TRANSITIONS_SORT_TYPE_FIXED: + case static::ENUM_TRANSITIONS_SORT_TYPE_RELATIVE: + $aSortedTransitions = $aTransitions; + + // Get states sorted as defined in the datamodel + $sStateAttCode = MetaModel::GetStateAttributeCode($sClass); + $oAttDef = MetaModel::GetAttributeDef($sClass, $sStateAttCode); + $aAllowedValues = $oAttDef->GetAllowedValues(); + $aStatesSortFromDatamodel = array_keys($aAllowedValues); + + // Sort $aSortedTransitions based on the states sort from the datamodel + uksort($aSortedTransitions, function($sKey1, $sKey2) use ($aSortedTransitions, $aStatesSortFromDatamodel) { + $sTargetState1 = $aSortedTransitions[$sKey1]['target_state']; + $sTargetState2 = $aSortedTransitions[$sKey2]['target_state']; + + return array_search($sTargetState1, $aStatesSortFromDatamodel) > array_search($sTargetState2, $aStatesSortFromDatamodel) ? 1 : -1; + }); + + if ($sSortType === static::ENUM_TRANSITIONS_SORT_TYPE_RELATIVE) { + // Find current state position + $sCurrentState = $this->Get($sStateAttCode); + $iCurrentStatePos = array_search($sCurrentState, $aStatesSortFromDatamodel); + + foreach ($aSortedTransitions as $sStimulusCode => $aTransitionDef) { + // Leave state with higher ranks than the current's in their positions + if (array_search($aTransitionDef['target_state'], $aStatesSortFromDatamodel) >= $iCurrentStatePos) { + continue; + } + + // Remove transition from beginning of the array and move it back at the end + array_shift($aSortedTransitions); + $aSortedTransitions = array_merge($aSortedTransitions, [$sStimulusCode => $aTransitionDef]); + } + } + break; + + default: + $aSortedTransitions = $aTransitions; + IssueLog::Error('Could not sort object transitions as the sort type is not correct. Check "lifecycle.transitions_sort_type" conf. parameter.', LogChannels::CORE, [ + 'lifecycle.transitions_sort_type' => $sSortType, + ]); + } + + return $aSortedTransitions; } /** diff --git a/core/valuesetdef.class.inc.php b/core/valuesetdef.class.inc.php index 2bac9e1b3..ef9ba674d 100644 --- a/core/valuesetdef.class.inc.php +++ b/core/valuesetdef.class.inc.php @@ -506,7 +506,6 @@ class ValueSetEnum extends ValueSetDefinition */ public function SortValues(array &$aValues): void { - // TODO: Add unit test // Force sort by values only if necessary if ($this->bSortByValue) { asort($aValues);