diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index 8b37484d5..35e172385 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -5918,6 +5918,14 @@ class AttributeEnum extends AttributeString $aLocalizedValues[$sKey] = $this->GetValueLabel($sKey); } + // Sort by label only if necessary + // See N°1646 and {@see \MFCompiler::CompileAttributeEnumValues()} for complete information as for why sort on labels is done at runtime while other sorting are done at compile time + /** @var \ValueSetEnum $oValueSetDef */ + $oValueSetDef = $this->GetValuesDef(); + if ($oValueSetDef->IsSortedByValues()) { + asort($aLocalizedValues); + } + return $aLocalizedValues; } diff --git a/core/valuesetdef.class.inc.php b/core/valuesetdef.class.inc.php index 5d854d638..2bac9e1b3 100644 --- a/core/valuesetdef.class.inc.php +++ b/core/valuesetdef.class.inc.php @@ -53,7 +53,13 @@ abstract class ValueSetDefinition return $sAllowedValues; } - + /** + * @param array $aArgs + * @param string $sContains + * @param string $sOperation for the values {@see static::LoadValues()} + * + * @return array hash array of keys => values + */ public function GetValues($aArgs, $sContains = '', $sOperation = 'contains') { if (!$this->m_bIsLoaded) @@ -78,11 +84,22 @@ abstract class ValueSetDefinition } } } - // Sort on the display value - asort($aRet); + $this->SortValues($aRet); return $aRet; } + /** + * @param array $aValues Values to sort in the form keys => values + * + * @return void + * @since 3.1.0 N°1646 Create method + */ + public function SortValues(array &$aValues): void + { + // Sort alphabetically on values + asort($aValues); + } + abstract protected function LoadValues($aArgs); } @@ -189,11 +206,7 @@ class ValueSetObjects extends ValueSetDefinition } /** - * @param $aArgs - * @param string $sContains - * @param string $sOperation for the values @see self::LoadValues() - * - * @return array + * @inheritDoc * @throws CoreException * @throws OQLException */ @@ -451,10 +464,33 @@ class ValueSetObjects extends ValueSetDefinition class ValueSetEnum extends ValueSetDefinition { protected $m_values; + /** + * @var bool $bSortByValue If true, values will be sorted at runtime, otherwise it is sorted at compile time in a predefined order. + * {@see \MFCompiler::CompileAttributeEnumValues()} for complete reasons. + * @since 3.1.0 N°1646 + */ + protected bool $bSortByValue; - public function __construct($Values) + /** + * @param array|string $Values + * @param bool $bLocalizedSort + * + * @since 3.1.0 N°1646 Add $bLocalizedSort parameter + */ + public function __construct($Values, bool $bSortByValue = false) { $this->m_values = $Values; + $this->bSortByValue = $bSortByValue; + } + + /** + * @see \ValueSetEnum::$bSortByValue + * @return bool + * @since 3.1.0 N°1646 + */ + public function IsSortedByValues(): bool + { + return $this->bSortByValue; } // Helper to export the data model @@ -464,6 +500,28 @@ class ValueSetEnum extends ValueSetDefinition return $this->m_aValues; } + /** + * @inheritDoc + * @since 3.1.0 N°1646 Overload method + */ + public function SortValues(array &$aValues): void + { + // TODO: Add unit test + // Force sort by values only if necessary + if ($this->bSortByValue) { + asort($aValues); + return; + } + + // Don't sort values as we rely on the order defined during compilation + return; + } + + /** + * @param array|string $aArgs + * + * @return true + */ protected function LoadValues($aArgs) { if (is_array($this->m_values)) diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php index 18a8bdac3..f8ad07896 100644 --- a/setup/compiler.class.inc.php +++ b/setup/compiler.class.inc.php @@ -54,6 +54,22 @@ class MFCompiler { const DATA_PRECOMPILED_FOLDER = 'data'.DIRECTORY_SEPARATOR.'precompiled_styles'.DIRECTORY_SEPARATOR; + /** + * @var string + * @since 3.1.0 + */ + protected const ENUM_ATTRIBUTE_ENUM_SORT_TYPE_CODE = 'code'; + /** + * @var string + * @since 3.1.0 + */ + protected const ENUM_ATTRIBUTE_ENUM_SORT_TYPE_LABEL = 'label'; + /** + * @var string + * @since 3.1.0 + */ + protected const ENUM_ATTRIBUTE_ENUM_SORT_TYPE_RANK = 'rank'; + /** * @var string * @see self::GenerateStyleDataFromNode @@ -2096,68 +2112,14 @@ EOF $this->CompileCommonProperty('allowed_values', $oField, $aParameters, $sModuleRelativeDir); $aParameters['depends_on'] = $sDependencies; } elseif ($sAttType == 'AttributeEnum') { - $oValues = $oField->GetUniqueElement('values'); - $oValueNodes = $oValues->getElementsByTagName('value'); - $aValues = []; - $aStyledValues = []; - foreach ($oValueNodes as $oValue) { - // New in 3.0 the format of values changed - $sCode = $this->GetMandatoryPropString($oValue, 'code', false); - $aValues[] = $sCode; - $oStyleNode = $oValue->GetOptionalElement('style'); - if ($oStyleNode) { - $aEnumStyleData = $this->GenerateStyleDataFromNode($oStyleNode, $sModuleRelativeDir, self::ENUM_STYLE_HOST_ELEMENT_TYPE_ENUM, $sClass, $sAttCode, $sCode); - $aStyledValues[] = $aEnumStyleData['orm_style_instantiation']; - $sCss .= $aEnumStyleData['scss']; - } - } - $sValues = '"'.implode(',', $aValues).'"'; - $aParameters['allowed_values'] = "new ValueSetEnum($sValues)"; - if (count($aStyledValues) > 0) { - $sStyledValues = '['.implode(',', $aStyledValues).']'; - $aParameters['styled_values'] = "$sStyledValues"; - } - $aParameters['allowed_values'] = "new ValueSetEnum($sValues)"; - $oDefaultStyleNode = $oField->GetOptionalElement('default_style'); - if ($oDefaultStyleNode) { - $aEnumStyleData = $this->GenerateStyleDataFromNode($oDefaultStyleNode, $sModuleRelativeDir, self::ENUM_STYLE_HOST_ELEMENT_TYPE_ENUM, $sClass, $sAttCode); - $aParameters['default_style'] = $aEnumStyleData['orm_style_instantiation']; - $sCss .= $aEnumStyleData['scss']; - } + $this->CompileAttributeEnumValues($sModuleRelativeDir, $sClass, $sAttCode, $oField, $aParameters, $sCss); $this->CompileCommonProperty('display_style', $oField, $aParameters, $sModuleRelativeDir, 'list'); $this->CompileCommonProperty('sql', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('default_value', $oField, $aParameters, $sModuleRelativeDir, ''); $this->CompileCommonProperty('is_null_allowed', $oField, $aParameters, $sModuleRelativeDir, false); $aParameters['depends_on'] = $sDependencies; } elseif ($sAttType == 'AttributeMetaEnum') { - $oValues = $oField->GetUniqueElement('values'); - $oValueNodes = $oValues->getElementsByTagName('value'); - $aValues = []; - $aStyledValues = []; - foreach ($oValueNodes as $oValue) { - // New in 3.0 the format of values changed - $sCode = $this->GetMandatoryPropString($oValue, 'code', false); - $aValues[] = $sCode; - $oStyleNode = $oValue->GetOptionalElement('style'); - if ($oStyleNode) { - $aEnumStyleData = $this->GenerateStyleDataFromNode($oStyleNode, $sModuleRelativeDir, self::ENUM_STYLE_HOST_ELEMENT_TYPE_ENUM, $sClass, $sAttCode, $sCode); - $aStyledValues[] = $aEnumStyleData['orm_style_instantiation']; - $sCss .= $aEnumStyleData['scss']; - } - } - $sValues = '"'.implode(',', $aValues).'"'; - $aParameters['allowed_values'] = "new ValueSetEnum($sValues)"; - if (count($aStyledValues) > 0) { - $sStyledValues = '['.implode(',', $aStyledValues).']'; - $aParameters['styled_values'] = "$sStyledValues"; - } - $aParameters['allowed_values'] = "new ValueSetEnum($sValues)"; - $oDefaultStyleNode = $oField->GetOptionalElement('default_style'); - if ($oDefaultStyleNode) { - $aEnumStyleData = $this->GenerateStyleDataFromNode($oDefaultStyleNode, $sModuleRelativeDir, self::ENUM_STYLE_HOST_ELEMENT_TYPE_ENUM, $sClass, $sAttCode); - $aParameters['default_style'] = $aEnumStyleData['orm_style_instantiation']; - $sCss .= $aEnumStyleData['scss']; - } + $this->CompileAttributeEnumValues($sModuleRelativeDir, $sClass, $sAttCode, $oField, $aParameters, $sCss); $this->CompileCommonProperty('sql', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('default_value', $oField, $aParameters, $sModuleRelativeDir, ''); $this->CompileCommonProperty('mappings', $oField, $aParameters, $sModuleRelativeDir); @@ -2507,6 +2469,78 @@ EOF $aParameters['depends_on'] = $sDependencies; } + protected function CompileAttributeEnumValues(string $sModuleRelativeDir, string $sClass, string $sAttCode, DOMElement $oField, array &$aParameters, string &$sCss): void + { + $sSortType = $oField->GetChildText('sort_type'); + if (utils::IsNullOrEmptyString($sSortType)) { + $sSortType = static::ENUM_ATTRIBUTE_ENUM_SORT_TYPE_CODE; + } + + $oValues = $oField->GetUniqueElement('values'); + $oValueNodes = $oValues->getElementsByTagName('value'); + $aValues = []; + $aValuesWithRank = []; + $aValuesWithoutRank = []; + $aStyledValues = []; + foreach ($oValueNodes as $oValue) { + // Value's code + $sCode = $this->GetMandatoryPropString($oValue, 'code', false); + $sRankAsString = $this->GetPropNumber($oValue, 'rank'); + // Consider value as ranked only if it is the desired sort type, this is to avoid issues if a node is left when sort type isn't "rank" + if (utils::IsNotNullOrEmptyString($sRankAsString) && ($sSortType === static::ENUM_ATTRIBUTE_ENUM_SORT_TYPE_RANK)){ + $aValuesWithRank[$sCode] = (float) $sRankAsString; + } else { + $aValuesWithoutRank[$sCode] = true; + } + + // Value's style + $oStyleNode = $oValue->GetOptionalElement('style'); + if ($oStyleNode) { + $aEnumStyleData = $this->GenerateStyleDataFromNode($oStyleNode, $sModuleRelativeDir, self::ENUM_STYLE_HOST_ELEMENT_TYPE_ENUM, $sClass, $sAttCode, $sCode); + $aStyledValues[] = $aEnumStyleData['orm_style_instantiation']; + $sCss .= $aEnumStyleData['scss']; + } + } + + // Order values + $aSortedValues = []; + $sLocalizedSortAsPHPParam = ''; + switch ($sSortType) { + case static::ENUM_ATTRIBUTE_ENUM_SORT_TYPE_RANK: + // Sort ranked values then append unranked values sorted by their code + asort($aValuesWithRank); + ksort($aValuesWithoutRank); + $aSortedValues = array_merge(array_keys($aValuesWithRank), array_keys($aValuesWithoutRank)); + break; + + case static::ENUM_ATTRIBUTE_ENUM_SORT_TYPE_LABEL: + // Sort by labels is delegated at runtime for one main reason: + // Default language (fallback -eg. english- if no dict entry for the current language -eg. italian-) can change at anytime in the configuration file -eg. from english to french- + // if that was to happen, users would not understand why they have labels from in english instead of french, which would cause support questions / investigations. + $sLocalizedSortAsPHPParam = ', true'; + default: + // Sort values by their code + ksort($aValuesWithoutRank); + $aSortedValues = array_keys($aValuesWithoutRank); + break; + } + + $sValuesAsPHPParam = '"'.implode(',', $aSortedValues).'"'; + $aParameters['allowed_values'] = "new ValueSetEnum($sValuesAsPHPParam $sLocalizedSortAsPHPParam)"; + if (count($aStyledValues) > 0) { + $sStyledValues = '['.implode(',', $aStyledValues).']'; + $aParameters['styled_values'] = "$sStyledValues"; + } + + // Default style for values + $oDefaultStyleNode = $oField->GetOptionalElement('default_style'); + if ($oDefaultStyleNode) { + $aEnumStyleData = $this->GenerateStyleDataFromNode($oDefaultStyleNode, $sModuleRelativeDir, self::ENUM_STYLE_HOST_ELEMENT_TYPE_ENUM, $sClass, $sAttCode); + $aParameters['default_style'] = $aEnumStyleData['orm_style_instantiation']; + $sCss .= $aEnumStyleData['scss']; + } + } + /** * @internal This method is public in order to be used in the tests * @@ -3415,7 +3449,7 @@ EOF; $aThemes[$sThemeId] = [ 'theme_parameters' => $aThemeParameters, - 'precompiled_stylesheet' => $oTheme->GetChildText('precompiled_stylesheet', '') + 'precompiled_stylesheet' => $oTheme->GetChildText('precompiled_stylesheet', ''), ]; } @@ -3523,7 +3557,7 @@ EOF; $aDirToCheck = [ $sSourceDir, - APPROOT . DIRECTORY_SEPARATOR . 'extensions/' + APPROOT . DIRECTORY_SEPARATOR . 'extensions/', ]; $iDataXmlFileLastModified = 0;