diff --git a/datamodels/2.x/itop-portal-base/en.dict.itop-portal-base.php b/datamodels/2.x/itop-portal-base/en.dict.itop-portal-base.php index 9a92f3abe..3831664e4 100644 --- a/datamodels/2.x/itop-portal-base/en.dict.itop-portal-base.php +++ b/datamodels/2.x/itop-portal-base/en.dict.itop-portal-base.php @@ -103,6 +103,17 @@ Dict::Add('EN US', 'English', 'English', array( 'Brick:Portal:Manage:Name' => 'Manage items', 'Brick:Portal:Manage:Table:NoData' => 'No item.', 'Brick:Portal:Manage:Table:ItemActions' => 'Actions', + 'Brick:Portal:Manage:DisplayType:pie-chart' => 'Pie Chart', + 'Brick:Portal:Manage:DisplayType:bar-chart' => 'Bar Chart', + 'Brick:Portal:Manage:DisplayType:default' => 'List', + 'Brick:Portal:Manage:Others' => 'Others', + 'Brick:Portal:Manage:All' => 'All', + 'Brick:Portal:Manage:Group' => 'Group', + 'Brick:Portal:Manage:fct:count' => 'Total', + 'Brick:Portal:Manage:fct:sum' => 'Sum', + 'Brick:Portal:Manage:fct:avg' => 'Average', + 'Brick:Portal:Manage:fct:min' => 'Min', + 'Brick:Portal:Manage:fct:max' => 'Max', )); // ObjectBrick brick diff --git a/datamodels/2.x/itop-portal-base/fr.dict.itop-portal-base.php b/datamodels/2.x/itop-portal-base/fr.dict.itop-portal-base.php index 3737f9f53..e84a5c30a 100644 --- a/datamodels/2.x/itop-portal-base/fr.dict.itop-portal-base.php +++ b/datamodels/2.x/itop-portal-base/fr.dict.itop-portal-base.php @@ -103,6 +103,17 @@ Dict::Add('FR FR', 'French', 'Français', array( 'Brick:Portal:Manage:Name' => 'Gestion d\'éléments', 'Brick:Portal:Manage:Table:NoData' => 'Aucun élément', 'Brick:Portal:Manage:Table:ItemActions' => 'Actions', + 'Brick:Portal:Manage:DisplayType:pie-chart' => 'Secteur', + 'Brick:Portal:Manage:DisplayType:bar-chart' => 'Histogramme', + 'Brick:Portal:Manage:DisplayType:default' => 'Liste', + 'Brick:Portal:Manage:Others' => 'Autres', + 'Brick:Portal:Manage:All' => 'Total', + 'Brick:Portal:Manage:Group' => 'Groupe', + 'Brick:Portal:Manage:fct:count' => 'Total', + 'Brick:Portal:Manage:fct:sum' => 'Somme', + 'Brick:Portal:Manage:fct:avg' => 'Moyenne', + 'Brick:Portal:Manage:fct:min' => 'Min', + 'Brick:Portal:Manage:fct:max' => 'Max', )); // ObjectBrick brick diff --git a/datamodels/2.x/itop-portal-base/module.itop-portal-base.php b/datamodels/2.x/itop-portal-base/module.itop-portal-base.php index eed1edb1c..5a79977dc 100644 --- a/datamodels/2.x/itop-portal-base/module.itop-portal-base.php +++ b/datamodels/2.x/itop-portal-base/module.itop-portal-base.php @@ -13,11 +13,12 @@ SetupWebPage::AddModule( 'visible' => true, // Components 'datamodel' => array( - 'portal/src/entities/abstractbrick.class.inc.php', - 'portal/src/entities/portalbrick.class.inc.php', + 'portal/src/apis/extensions/d3portaluiextension.class.inc.php', 'portal/src/controllers/abstractcontroller.class.inc.php', 'portal/src/controllers/brickcontroller.class.inc.php', - 'portal/src/routers/abstractrouter.class.inc.php', + 'portal/src/entities/abstractbrick.class.inc.php', + 'portal/src/entities/portalbrick.class.inc.php', + 'portal/src/routers/abstractrouter.class.inc.php', ), 'webservice' => array( //'webservices.itop-portal-base.php', diff --git a/datamodels/2.x/itop-portal-base/portal/src/apis/extensions/d3portaluiextension.class.inc.php b/datamodels/2.x/itop-portal-base/portal/src/apis/extensions/d3portaluiextension.class.inc.php new file mode 100644 index 000000000..f105f9c54 --- /dev/null +++ b/datamodels/2.x/itop-portal-base/portal/src/apis/extensions/d3portaluiextension.class.inc.php @@ -0,0 +1,44 @@ + +// +namespace Combodo\iTop\Portal\API\Extension; + +use AbstractPortalUIExtension; +use Silex\Application; +use utils; + +class D3PortalUIExtension extends AbstractPortalUIExtension +{ + public function GetCSSFiles(Application $oApp) + { + $aCSSFiles = array( + utils::GetAbsoluteUrlAppRoot().'css/c3.min.css?v='.ITOP_VERSION, + ); + return $aCSSFiles; + } + + public function GetJSFiles(Application $oApp) + { + $aJSFiles = array( + utils::GetAbsoluteUrlAppRoot().'js/d3.min.js?v='.ITOP_VERSION, + utils::GetAbsoluteUrlAppRoot().'js/c3.min.js?v='.ITOP_VERSION, + utils::GetCurrentModuleUrl().'/portal/web/js/export.js?v='.ITOP_VERSION, + ); + return $aJSFiles; + } +} \ No newline at end of file diff --git a/datamodels/2.x/itop-portal-base/portal/src/controllers/managebrickcontroller.class.inc.php b/datamodels/2.x/itop-portal-base/portal/src/controllers/managebrickcontroller.class.inc.php index 62eeef9e0..85661f2e9 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/controllers/managebrickcontroller.class.inc.php +++ b/datamodels/2.x/itop-portal-base/portal/src/controllers/managebrickcontroller.class.inc.php @@ -1,6 +1,6 @@ GetDisplayType(); + } + $aDisplayParams = $oBrick->GetPresentationDataForDisplayType($sDisplayType); + $aData = $this->GetData($oRequest, $oApp, $sBrickId, $sGroupingTab, $aDisplayParams['need_details']); + + $aExportFields = $oBrick->GetExportFields(); + $aData = $aData + array( + 'sDisplayType' => $sDisplayType, + 'bCanExport' => !empty($aExportFields), + ); + // Preparing response + if ($oRequest->isXmlHttpRequest()) + { + $oResponse = $oApp->json($aData); + } + else + { + $sLayoutTemplate = $aDisplayParams['layoutTemplate']; + $oResponse = $oApp['twig']->render($sLayoutTemplate, $aData); + } + + return $oResponse; + } + + /** + * Method for the brick's tile on home page + * + * @param Request $oRequest + * @param Application $oApp + * @param string $sBrickId + * + * @return Response + */ + public function TileAction(Request $oRequest, Application $oApp, $sBrickId) { - /** @var \Combodo\iTop\Portal\Brick\ManageBrick $oBrick */ + /** @var ManageBrick $oBrick */ + $oBrick = ApplicationHelper::GetLoadedBrickFromId($oApp, $sBrickId); + + try + { + $aData = $this->GetData($oRequest, $oApp, $sBrickId, null); + } + catch (\Exception $e) + { + // TODO Default values + $aData = array(); + } + + return $oApp['twig']->render($oBrick->GetTileTemplatePath(), $aData); + } + + /** + * @param Request $oRequest + * @param Application $oApp + * @param string $sBrickId + * @param string $sGroupingTab + * @param string $sGroupingArea + * + * @throws \DictExceptionMissingString + * @throws \MissingQueryArgument + * @throws \MySQLException + * @throws \OQLException + * @throws \Exception + */ + public function ExcelExportStartAction( + Request $oRequest, Application $oApp, $sBrickId, $sGroupingTab, $sGroupingArea + ) { + /** @var ManageBrick $oBrick */ + $oBrick = ApplicationHelper::GetLoadedBrickFromId($oApp, $sBrickId); + $oQuery = DBSearch::FromOQL($oBrick->GetOql()); + $sClass = $oQuery->GetClass(); + $aData = $this->GetData($oRequest, $oApp, $sBrickId, $sGroupingTab, true); + + if (isset($aData['aQueries']) && count($aData['aQueries']) === 1) + { + $aQueries = $aData['aQueries']; + reset($aQueries); + $sKey = key($aQueries); + $oSearch = $aData['aQueries'][$sKey]; + } + else + { + $oQuery = DBSearch::FromOQL($oBrick->GetOql()); + $sClass = $oQuery->GetClass(); + $this->AddScopeToQuery($oQuery, $oApp, $oBrick, $sClass); + $aData = array(); + $this->ManageSearchValue($oRequest, $aData, $oQuery, $sClass); + + // Grouping tab + if ($oBrick->HasGroupingTabs()) + { + $aGroupingTabs = $oBrick->GetGroupingTabs(); + + // If tabs are made of the distinct values of an attribute, we have a find them via a query + if ($oBrick->IsGroupingTabsByDistinctValues()) + { + $sGroupingTabAttCode = $aGroupingTabs['attribute']; + $aGroupingTabsValues = $this->GroupByAttribute($oQuery, $sGroupingTabAttCode, $oApp, $oBrick); + $oQuery = $oQuery->Intersect($aGroupingTabsValues[$sGroupingTab]['condition']); + } + else + { + foreach ($aGroupingTabs['groups'] as $aGroup) + { + if ($aGroup['id'] === $sGroupingTab) + { + $oConditionQuery = $oQuery->Intersect(DBSearch::FromOQL($aGroup['condition'])); + $oQuery = $oQuery->Intersect($oConditionQuery); + break; + } + } + } + } + + // Finalclass + $oConditionQuery = DBSearch::CloneWithAlias($oQuery, 'GARE'); + $oExpression = new BinaryExpression(new FieldExpression('finalclass', 'GARE'), '=', + new UnaryExpression($sGroupingArea)); + $oConditionQuery->AddConditionExpression($oExpression); + /** @var DBSearch $oSearch */ + $oSearch = $oQuery->Intersect($oConditionQuery); + } + + $aColumnsAttrs = $oBrick->GetExportFields(); + $aFields = array(); + $sTitleAttrCode = 'friendlyname'; + if (!in_array($sTitleAttrCode, $aColumnsAttrs)) + { + $aFields[] = $sTitleAttrCode; + } + foreach ($aColumnsAttrs as $sAttCode) + { + $oAttributeDef = MetaModel::GetAttributeDef($sGroupingArea, $sAttCode); + if ($oAttributeDef->IsExternalKey(EXTKEY_ABSOLUTE)) + { + $aFields[] = $sAttCode.'_friendlyname'; + } + else + { + $aFields[] = $sAttCode; + } + } + + $sFields = implode(',', $aFields); + $aData = array( + 'oBrick' => $oBrick, + 'sBrickId' => $sBrickId, + 'sFields' => $sFields, + 'sOQL' => $oSearch->ToOQL(), + ); + + return $oApp['twig']->render(static::EXCEL_EXPORT_TEMPLATE_PATH, $aData); + } + + + /** + * @param Request $oRequest + * @param Application $oApp + * @param string $sBrickId + * @param string $sGroupingTab + * @param bool $bNeedDetails + * + * @return array + * @throws \CoreException + * @throws \DictExceptionMissingString + * @throws \Exception + * @throws \MissingQueryArgument + * @throws \MySQLException + * @throws \OQLException + */ + public function GetData(Request $oRequest, Application $oApp, $sBrickId, $sGroupingTab, $bNeedDetails = false) + { + /** @var ManageBrick $oBrick */ $oBrick = ApplicationHelper::GetLoadedBrickFromId($oApp, $sBrickId); $aData = array(); $aGroupingTabsValues = array(); $aGroupingAreasValues = array(); $aQueries = array(); + $bHasScope = true; // Getting current dataloading mode (First from router parameter, then query parameter, then default brick value) - $sDataLoading = ($sDataLoading !== null) ? $sDataLoading : ( ($oRequest->get('sDataLoading') !== null) ? $oRequest->get('sDataLoading') : $oBrick->GetDataLoading() ); - // Getting search value - $sSearchValue = $oRequest->get('sSearchValue', null); + $sDataLoading = ($oRequest->get('sDataLoading') !== null) ? $oRequest->get('sDataLoading') : $oBrick->GetDataLoading(); + + // - Retrieving the grouping areas to display + $sGroupingArea = $oRequest->get('sGroupingArea'); + if (!is_null($sGroupingArea)) + { + $bNeedDetails = true; + } // Getting area columns properties $aColumnsAttrs = $oBrick->GetFields(); - // Adding friendlyname attribute to the list is not already in it + // Adding friendlyname attribute to the list if not already in it $sTitleAttrCode = 'friendlyname'; if (($sTitleAttrCode !== null) && !in_array($sTitleAttrCode, $aColumnsAttrs)) { @@ -78,45 +272,15 @@ class ManageBrickController extends BrickController // Starting to build query $oQuery = DBSearch::FromOQL($oBrick->GetOql()); + $sClass = $oQuery->GetClass(); + $sIconURL = \MetaModel::GetClassIcon($sClass, false); // - Adding search clause if necessary - // Note : This is a very naive search at the moment - if ($sSearchValue !== null) - { - $aSearchListItems = MetaModel::GetZListItems($oQuery->GetClass(), 'standard_search'); - $oFullBinExpr = null; - for ($i = 0; $i < count($aSearchListItems); $i++) - { - $sSearchItemAttr = $aSearchListItems[$i]; - $oBinExpr = new BinaryExpression(new FieldExpression($sSearchItemAttr, $oQuery->GetClassAlias()), 'LIKE', new VariableExpression('search_value')); - - // At each iteration we build the complete expression for the search like ( (field1 LIKE %search%) OR (field2 LIKE %search%) OR (field3 LIKE %search%) ...) - if ($i === 0) - { - $oFullBinExpr = $oBinExpr; - } - else - { - $oFullBinExpr = new BinaryExpression($oFullBinExpr, 'OR', $oBinExpr); - } - - // Then on the last iteration we add the complete expression to the query - // Note : We don't do it after the loop as there could be an empty search ZList - if ($i === (count($aSearchListItems) - 1)) - { - // - Adding expression to the query - $oQuery->AddConditionExpression($oFullBinExpr); - // - Setting expression parameters - // Note : This could be way more simpler if we had a SetInternalParam($sParam, $value) verb - $aQueryParams = $oQuery->GetInternalParams(); - $aQueryParams['search_value'] = '%' . $sSearchValue . '%'; - $oQuery->SetInternalParams($aQueryParams); - } - } - } + $this->ManageSearchValue($oRequest, $aData, $oQuery, $sClass); // Preparing tabs // - We need to retrieve distinct values for the grouping attribute + $iCount = 0; if ($oBrick->HasGroupingTabs()) { $aGroupingTabs = $oBrick->GetGroupingTabs(); @@ -125,50 +289,10 @@ class ManageBrickController extends BrickController if ($oBrick->IsGroupingTabsByDistinctValues()) { $sGroupingTabAttCode = $aGroupingTabs['attribute']; - - $oDistinctQuery = DBSearch::FromOQL($oBrick->GetOql()); - // - Restricting query to scope - $oScopeQuery = $oApp['scope_validator']->GetScopeFilterForProfiles(UserRights::ListProfiles(), $oDistinctQuery->GetClass(), UR_ACTION_READ); - if ($oScopeQuery !== null) - { - $oDistinctQuery = $oDistinctQuery->Intersect($oScopeQuery); - // - Allowing all data if necessary - if ($oScopeQuery->IsAllDataAllowed()) - { - $oDistinctQuery->AllowAllData(); - } - } - // - Adding field condition - $oFieldExp = new FieldExpression($sGroupingTabAttCode, $oDistinctQuery->GetClassAlias()); - $sDistinctSql = $oDistinctQuery->MakeGroupByQuery(array(), array('grouped_by_1' => $oFieldExp), true); - $aDistinctResults = CMDBSource::QueryToArray($sDistinctSql); - - if (!empty($aDistinctResults)) + $aGroupingTabsValues = $this->GroupByAttribute($oQuery, $sGroupingTabAttCode, $oApp, $oBrick); + foreach ($aGroupingTabsValues as $aResult) { - foreach ($aDistinctResults as $aDistinctResult) - { - $oConditionQuery = DBSearch::CloneWithAlias($oQuery, 'GTAB'); - $oExpression = new BinaryExpression(new FieldExpression($sGroupingTabAttCode, $oDistinctQuery->GetClassAlias()), '=', new UnaryExpression($aDistinctResult['grouped_by_1'])); - $oConditionQuery->AddConditionExpression($oExpression); - - $aGroupingTabsValues[$aDistinctResult['grouped_by_1']] = array( - 'value' => $aDistinctResult['grouped_by_1'], - 'label' => strip_tags($oFieldExp->MakeValueLabel($oDistinctQuery, $aDistinctResult['grouped_by_1'], '')), - 'condition' => $oConditionQuery, - 'count' => $aDistinctResult['_itop_count_'], - ); - unset($oConditionQuery); - } - unset($aDistinctResults); - } - else - { - $aGroupingTabsValues['undefined'] = array( - 'value' => 'undefined', - 'label' => '', - 'condition' => null, - 'count' => null, - ); + $iCount += $aResult['count']; } } // Otherwise we create the tabs from the SQL expressions @@ -176,30 +300,40 @@ class ManageBrickController extends BrickController { foreach ($aGroupingTabs['groups'] as $aGroup) { - $oConditionQuery = $oQuery->Intersect( DBSearch::FromOQL($aGroup['condition']) ); - // - Restricting query to scope - $oScopeQuery = $oApp['scope_validator']->GetScopeFilterForProfiles(UserRights::ListProfiles(), $oConditionQuery->GetClass(), UR_ACTION_READ); - if ($oScopeQuery !== null) - { - $oConditionQuery = $oConditionQuery->Intersect($oScopeQuery); - // - Allowing all data if necessary - if ($oScopeQuery->IsAllDataAllowed()) - { - $oConditionQuery->AllowAllData(); - } - } - // - Building ObjectSet - $oConditionSet = new DBObjectSet($oConditionQuery); - + $oConditionQuery = $oQuery->Intersect(DBSearch::FromOQL($aGroup['condition'])); + // - Restricting query to scope + if ($this->AddScopeToQuery($oConditionQuery, $oApp, $oBrick, $oConditionQuery->GetClass())) + { + // - Building ObjectSet + $oConditionSet = new DBObjectSet($oConditionQuery); + $iGroupCount = $oConditionSet->Count(); + } + else + { + $oConditionSet = null; + $iGroupCount = 0; + } $aGroupingTabsValues[$aGroup['id']] = array( 'value' => $aGroup['id'], 'label' => Dict::S($aGroup['title']), + 'label_html' => Dict::S($aGroup['title']), 'condition' => $oConditionQuery, - 'count' => $oConditionSet->Count(), + 'count' => $iGroupCount, ); + $iCount += $iGroupCount; } } } + else + { + $oConditionQuery = $this->GetScopedQuery($oApp, $oBrick, $sClass); + if (!is_null($oConditionQuery)) + { + $oSet = new DBObjectSet($oConditionQuery); + $iCount = $oSet->Count(); + } + } + // - Retrieving the current grouping tab to display and altering the query to do so if ($sGroupingTab === null) { @@ -212,10 +346,6 @@ class ManageBrickController extends BrickController $oQuery = $oQuery->Intersect($aGroupingTabsValues[$sGroupingTab]['condition']); } } - else - { - // Do not group by tabs, display all in the same page - } } else { @@ -229,25 +359,14 @@ class ManageBrickController extends BrickController // - We need to retrieve distinct values for the grouping attribute // Note : Will have to be changed when we consider grouping on something else than the finalclass $sParentAlias = $oQuery->GetClassAlias(); - if (true) + if ($bNeedDetails) { $sGroupingAreaAttCode = 'finalclass'; // For root classes - if (MetaModel::IsValidAttCode($oQuery->GetClass(), $sGroupingAreaAttCode)) + if (MetaModel::IsValidAttCode($sClass, $sGroupingAreaAttCode)) { - $oDistinctQuery = DBSearch::FromOQL($oBrick->GetOql()); - // Checking if there is a scope to apply - $oDistinctScopeQuery = $oApp['scope_validator']->GetScopeFilterForProfiles(UserRights::ListProfiles(), $oQuery->GetClass(), UR_ACTION_READ); - if ($oDistinctScopeQuery != null) - { - $oDistinctQuery = $oDistinctQuery->Intersect($oDistinctScopeQuery); - // - Allowing all data if necessary - if ($oDistinctScopeQuery->IsAllDataAllowed()) - { - $oDistinctQuery->AllowAllData(); - } - } + $oDistinctQuery = $this->GetScopedQuery($oApp, $oBrick, $sClass); // Adding grouping conditions $oFieldExp = new FieldExpression($sGroupingAreaAttCode, $sParentAlias); $sDistinctSql = $oDistinctQuery->MakeGroupByQuery(array(), array('grouped_by_1' => $oFieldExp), true); @@ -256,12 +375,14 @@ class ManageBrickController extends BrickController foreach ($aDistinctResults as $aDistinctResult) { $oConditionQuery = DBSearch::CloneWithAlias($oQuery, 'GARE'); - $oExpression = new BinaryExpression(new FieldExpression($sGroupingAreaAttCode, 'GARE'), '=', new UnaryExpression($aDistinctResult['grouped_by_1'])); + $oExpression = new BinaryExpression(new FieldExpression($sGroupingAreaAttCode, 'GARE'), '=', + new UnaryExpression($aDistinctResult['grouped_by_1'])); $oConditionQuery->AddConditionExpression($oExpression); $aGroupingAreasValues[$aDistinctResult['grouped_by_1']] = array( 'value' => $aDistinctResult['grouped_by_1'], - 'label' => MetaModel::GetName($aDistinctResult['grouped_by_1']), // Caution : This works only because we froze the grouping areas on the finalclass attribute. + 'label' => MetaModel::GetName($aDistinctResult['grouped_by_1']), + // Caution : This works only because we froze the grouping areas on the finalclass attribute. 'condition' => $oConditionQuery, 'count' => $aDistinctResult['_itop_count_'] ); @@ -272,296 +393,544 @@ class ManageBrickController extends BrickController // For leaf classes else { - $aGroupingAreasValues[$oQuery->GetClass()] = array( - 'value' => $oQuery->GetClass(), - 'label' => MetaModel::GetName($oQuery->GetClass()), // Caution : This works only because we froze the grouping areas on the finalclass attribute. + $aGroupingAreasValues[$sClass] = array( + 'value' => $sClass, + 'label' => MetaModel::GetName($sClass), + // Caution : This works only because we froze the grouping areas on the finalclass attribute. 'condition' => null, 'count' => 0 ); } - } - // - Retrieving the grouping areas to display - $sGroupingArea = $oRequest->get('sGroupingArea'); - // - If specified or lazy loading, we trunc the $aGroupingAreasValues to keep only this one - if ($sGroupingArea !== null) - { - $aGroupingAreasValues = array($sGroupingArea => $aGroupingAreasValues[$sGroupingArea]); - } - // - Preapring the queries - foreach ($aGroupingAreasValues as $sKey => $aGroupingAreasValue) - { - $oAreaQuery = DBSearch::CloneWithAlias($oQuery, $sParentAlias); - if ($aGroupingAreasValue['condition'] !== null) + + // - If specified or lazy loading, we truncate the $aGroupingAreasValues to keep only this one + if ($sGroupingArea !== null) { - //$oAreaQuery->AddConditionExpression($aGroupingAreasValue['condition']); - $oAreaQuery = $oAreaQuery->Intersect($aGroupingAreasValue['condition']); + $aGroupingAreasValues = array($sGroupingArea => $aGroupingAreasValues[$sGroupingArea]); } - - // Restricting query to allowed scope on each classes - // Note: Will need to moved the scope restriction on queries elsewhere when we consider grouping on something else than finalclass - // Note: We now get view scope instead of edit scope as we allowed users to view/edit objects in the brick regarding their rights - $oScopeQuery = $oApp['scope_validator']->GetScopeFilterForProfiles(UserRights::ListProfiles(), $aGroupingAreasValue['value'], UR_ACTION_READ); - if ($oScopeQuery !== null) + // - Preparing the queries + foreach ($aGroupingAreasValues as $sKey => $aGroupingAreasValue) { - $oAreaQuery = $oAreaQuery->Intersect($oScopeQuery); - // - Allowing all data if necessary - if ($oScopeQuery->IsAllDataAllowed()) + $oAreaQuery = DBSearch::CloneWithAlias($oQuery, $sParentAlias); + if ($aGroupingAreasValue['condition'] !== null) { - $oAreaQuery->AllowAllData(); - } - } - else - { - $oAreaQuery = null; - } - - $aQueries[$sKey] = $oAreaQuery; - } - - // Testing appropriate data loading mode if we are in auto - // - For all (html) tables, this doesn't care for the grouping ares (finalclass) - if ($sDataLoading === AbstractBrick::ENUM_DATA_LOADING_AUTO) - { - // - Check how many records there is. - // - Update $sDataLoading with its new value regarding the number of record and the threshold - $oCountSet = new DBObjectSet($oQuery); - $oCountSet->OptimizeColumnLoad(array()); - $fThreshold = (float) MetaModel::GetModuleSetting($oApp['combodo.portal.instance.id'], 'lazy_loading_threshold'); - $sDataLoading = ($oCountSet->Count() > $fThreshold) ? AbstractBrick::ENUM_DATA_LOADING_LAZY : AbstractBrick::ENUM_DATA_LOADING_FULL; - unset($oCountSet); - } - - // Preparing data sets - $aSets = array(); - /** @var DBSearch $oQuery */ - foreach ($aQueries as $sKey => $oQuery) - { - // Checking if we have a valid query - if ($oQuery !== null) - { - // Setting query pagination if needed - if ($sDataLoading === AbstractBrick::ENUM_DATA_LOADING_LAZY) - { - // Retrieving parameters - $iPageNumber = (int) $oRequest->get('iPageNumber', 1); - $iListLength = (int) $oRequest->get('iListLength', ManageBrick::DEFAULT_LIST_LENGTH); - - // Getting total records number - $oCountSet = new DBObjectSet($oQuery); - $oCountSet->OptimizeColumnLoad(array($oQuery->GetClassAlias() => $aColumnsAttrs)); - $aData['recordsTotal'] = $oCountSet->Count(); - $aData['recordsFiltered'] = $oCountSet->Count(); - unset($oCountSet); - - $oSet = new DBObjectSet($oQuery); - $oSet->SetLimit($iListLength, $iListLength * ($iPageNumber - 1)); - } - else - { - $oSet = new DBObjectSet($oQuery); + //$oAreaQuery->AddConditionExpression($aGroupingAreasValue['condition']); + $oAreaQuery = $oAreaQuery->Intersect($aGroupingAreasValue['condition']); } - // Adding always_in_tables attributes - $aColumnsToLoad = array($oQuery->GetClassAlias() => $aColumnsAttrs); - foreach($oQuery->GetSelectedClasses() as $sAlias => $sClass) - { - /** @var AttributeDefinition $oAttDef */ - foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) - { - if($oAttDef->AlwaysLoadInTables()) - { - $aColumnsToLoad[$sAlias][] = $sAttCode; - } - } - } - - $oSet->OptimizeColumnLoad($aColumnsToLoad); - $oSet->SetOrderByClasses(); - SecurityHelper::PreloadForCache($oApp, $oSet->GetFilter(), $aColumnsToLoad[$oQuery->GetClassAlias()] /* preloading only extkeys from the main class */); - $aSets[$sKey] = $oSet; - } - } - - // Retrieving and preparing data for rendering - $aGroupingAreasData = array(); - $bHasObjectListItemExtension = false; - foreach ($aSets as $sKey => $oSet) - { - // Set properties - $sCurrentClass = $sKey; - - // Defining which attribute will open the edition form) - $sMainActionAttrCode = $aColumnsAttrs[0]; - - // Loading columns definition - $aColumnsDefinition = array(); - foreach ($aColumnsAttrs as $sColumnAttr) - { - $oAttDef = MetaModel::GetAttributeDef($sKey, $sColumnAttr); - $aColumnsDefinition[$sColumnAttr] = array( - 'title' => $oAttDef->GetLabel(), - 'type' => ($oAttDef instanceof AttributeDateTime) ? 'moment-'.$oAttDef->GetFormat()->ToMomentJS() : 'html', // Special sorting for Date & Time - ); - } - - // Getting items - $aItems = array(); - // ... For each item - /** @var DBObject $oCurrentRow */ - while ($oCurrentRow = $oSet->Fetch()) - { - // ... Retrieving item's attributes values - $aItemAttrs = array(); - foreach ($aColumnsAttrs as $sItemAttr) + // Restricting query to allowed scope on each classes + // Note: Will need to moved the scope restriction on queries elsewhere when we consider grouping on something else than finalclass + // Note: We now get view scope instead of edit scope as we allowed users to view/edit objects in the brick regarding their rights + if (!$this->AddScopeToQuery($oAreaQuery, $oApp, $oBrick, $aGroupingAreasValue['value'])) { - $aActions = array(); - // Set the edit action to the main (first) attribute only - //if ($sItemAttr === $sTitleAttrCode) - if ($sItemAttr === $sMainActionAttrCode) - { - // Checking if we can edit the object - if (($oBrick->GetOpeningMode() === ManageBrick::ENUM_ACTION_EDIT) && SecurityHelper::IsActionAllowed($oApp, UR_ACTION_MODIFY, $sCurrentClass, $oCurrentRow->GetKey())) - { - $sActionType = ManageBrick::ENUM_ACTION_EDIT; - } - // - Otherwise, check if view is allowed - elseif (SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $sCurrentClass, $oCurrentRow->GetKey())) - { - $sActionType = ManageBrick::ENUM_ACTION_VIEW; - } - else - { - $sActionType = null; - } - // - Then set allowed action - if ($sActionType !== null) - { - $aActions[] = array( - 'type' => $sActionType, - 'class' => $sCurrentClass, - 'id' => $oCurrentRow->GetKey(), - 'opening_target' => $oBrick->GetOpeningTarget(), - ); - } - } + // if no scope apply does not allow any data + $oAreaQuery = null; + } - /** @var AttributeDefinition $oAttDef */ - $oAttDef = MetaModel::GetAttributeDef($sCurrentClass, $sItemAttr); - if ($oAttDef->IsExternalKey()) - { - $sValue = $oCurrentRow->Get($sItemAttr . '_friendlyname'); + $aQueries[$sKey] = $oAreaQuery; + } - // Adding a view action on the external keys - if ($oCurrentRow->Get($sItemAttr) !== $oAttDef->GetNullValue()) - { - // Checking if we can view the object - if ((SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $oAttDef->GetTargetClass(), $oCurrentRow->Get($sItemAttr)))) - { - $aActions[] = array( - 'type' => ManageBrick::ENUM_ACTION_VIEW, - 'class' => $oAttDef->GetTargetClass(), - 'id' => $oCurrentRow->Get($sItemAttr), - 'opening_target' => $oBrick->GetOpeningTarget(), - ); - } - } - } - elseif ($oAttDef instanceof AttributeSubItem || $oAttDef instanceof AttributeDuration) + $aData['aQueries'] = $aQueries; + + // Testing appropriate data loading mode if we are in auto + // - For all (html) tables, this doesn't care for the grouping ares (finalclass) + if ($sDataLoading === AbstractBrick::ENUM_DATA_LOADING_AUTO) + { + // - Check how many records there is. + // - Update $sDataLoading with its new value regarding the number of record and the threshold + $oCountSet = new DBObjectSet($oQuery); + $oCountSet->OptimizeColumnLoad(array()); + $fThreshold = (float)MetaModel::GetModuleSetting($oApp['combodo.portal.instance.id'], + 'lazy_loading_threshold'); + $sDataLoading = ($oCountSet->Count() > $fThreshold) ? AbstractBrick::ENUM_DATA_LOADING_LAZY : AbstractBrick::ENUM_DATA_LOADING_FULL; + unset($oCountSet); + } + + // Preparing data sets + $aSets = array(); + /** @var DBSearch $oQuery */ + foreach ($aQueries as $sKey => $oQuery) + { + // Checking if we have a valid query + if ($oQuery !== null) + { + // Setting query pagination if needed + if ($sDataLoading === AbstractBrick::ENUM_DATA_LOADING_LAZY) { - $sValue = $oAttDef->GetAsHTML($oCurrentRow->Get($sItemAttr)); + // Retrieving parameters + $iPageNumber = (int)$oRequest->get('iPageNumber', 1); + $iListLength = (int)$oRequest->get('iListLength', ManageBrick::DEFAULT_LIST_LENGTH); + + // Getting total records number + $oCountSet = new DBObjectSet($oQuery); + $oCountSet->OptimizeColumnLoad(array($oQuery->GetClassAlias() => $aColumnsAttrs)); + $aData['recordsTotal'] = $oCountSet->Count(); + $aData['recordsFiltered'] = $oCountSet->Count(); + unset($oCountSet); + + $oSet = new DBObjectSet($oQuery); + $oSet->SetLimit($iListLength, $iListLength * ($iPageNumber - 1)); } else { - $sValue = $oAttDef->GetValueLabel($oCurrentRow->Get($sItemAttr)); + $oSet = new DBObjectSet($oQuery); } - unset($oAttDef); - $aItemAttrs[$sItemAttr] = array( - 'att_code' => $sItemAttr, - 'value' => $sValue, - 'actions' => $aActions + // Adding always_in_tables attributes + $aColumnsToLoad = array($oQuery->GetClassAlias() => $aColumnsAttrs); + foreach ($oQuery->GetSelectedClasses() as $sAlias => $sClassSelected) + { + /** @var AttributeDefinition $oAttDef */ + foreach (MetaModel::ListAttributeDefs($sClassSelected) as $sAttCode => $oAttDef) + { + if ($oAttDef->AlwaysLoadInTables()) + { + $aColumnsToLoad[$sAlias][] = $sAttCode; + } + } + } + + $oSet->OptimizeColumnLoad($aColumnsToLoad); + $oSet->SetOrderByClasses(); + SecurityHelper::PreloadForCache($oApp, $oSet->GetFilter(), + $aColumnsToLoad[$oQuery->GetClassAlias()] /* preloading only extkeys from the main class */); + $aSets[$sKey] = $oSet; + } + } + + // Retrieving and preparing data for rendering + $aGroupingAreasData = array(); + $bHasObjectListItemExtension = false; + foreach ($aSets as $sKey => $oSet) + { + // Set properties + $sCurrentClass = $sKey; + + // Defining which attribute will open the edition form) + $sMainActionAttrCode = $aColumnsAttrs[0]; + + // Loading columns definition + $aColumnsDefinition = array(); + foreach ($aColumnsAttrs as $sColumnAttr) + { + $oAttDef = MetaModel::GetAttributeDef($sKey, $sColumnAttr); + $aColumnsDefinition[$sColumnAttr] = array( + 'title' => $oAttDef->GetLabel(), + 'type' => ($oAttDef instanceof AttributeDateTime) ? 'moment-'.$oAttDef->GetFormat()->ToMomentJS() : 'html', + // Special sorting for Date & Time ); } - // ... Checking menu extensions - $aItemButtons = array(); - foreach (MetaModel::EnumPlugins('iPopupMenuExtension') as $oExtensionInstance) - { - foreach($oExtensionInstance->EnumItems(iPopupMenuExtension::PORTAL_OBJLISTITEM_ACTIONS, array('portal_id' => $oApp['combodo.portal.instance.id'], 'object' => $oCurrentRow)) as $oMenuItem) - { - if (is_object($oMenuItem)) - { - if($oMenuItem instanceof JSButtonItem) - { - $aItemButtons[] = $oMenuItem->GetMenuItem() + array('js_files' => $oMenuItem->GetLinkedScripts(), 'type' => 'button'); - } - elseif($oMenuItem instanceof URLButtonItem) - { - $aItemButtons[] = $oMenuItem->GetMenuItem() + array('type' => 'link'); - } - } - } - } - - // ... And item's properties - $aItems[] = array( - 'id' => $oCurrentRow->GetKey(), - 'class' => $sCurrentClass, - 'attributes' => $aItemAttrs, - 'highlight_class' => $oCurrentRow->GetHilightClass(), - 'actions' => $aItemButtons, + // Getting items + $aItems = array(); + // ... For each item + /** @var DBObject $oCurrentRow */ + while ($oCurrentRow = $oSet->Fetch()) + { + // ... Retrieving item's attributes values + $aItemAttrs = array(); + foreach ($aColumnsAttrs as $sItemAttr) + { + $aActions = array(); + // Set the edit action to the main (first) attribute only + //if ($sItemAttr === $sTitleAttrCode) + if ($sItemAttr === $sMainActionAttrCode) + { + // Checking if we can edit the object + if (($oBrick->GetOpeningMode() === ManageBrick::ENUM_ACTION_EDIT) && SecurityHelper::IsActionAllowed($oApp, + UR_ACTION_MODIFY, $sCurrentClass, $oCurrentRow->GetKey())) + { + $sActionType = ManageBrick::ENUM_ACTION_EDIT; + } + // - Otherwise, check if view is allowed + elseif (SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $sCurrentClass, + $oCurrentRow->GetKey())) + { + $sActionType = ManageBrick::ENUM_ACTION_VIEW; + } + else + { + $sActionType = null; + } + // - Then set allowed action + if ($sActionType !== null) + { + $aActions[] = array( + 'type' => $sActionType, + 'class' => $sCurrentClass, + 'id' => $oCurrentRow->GetKey(), + 'opening_target' => $oBrick->GetOpeningTarget(), + ); + } + } + + /** @var AttributeDefinition $oAttDef */ + $oAttDef = MetaModel::GetAttributeDef($sCurrentClass, $sItemAttr); + if ($oAttDef->IsExternalKey()) + { + $sValue = $oCurrentRow->Get($sItemAttr.'_friendlyname'); + + // Adding a view action on the external keys + if ($oCurrentRow->Get($sItemAttr) !== $oAttDef->GetNullValue()) + { + // Checking if we can view the object + if ((SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $oAttDef->GetTargetClass(), + $oCurrentRow->Get($sItemAttr)))) + { + $aActions[] = array( + 'type' => ManageBrick::ENUM_ACTION_VIEW, + 'class' => $oAttDef->GetTargetClass(), + 'id' => $oCurrentRow->Get($sItemAttr), + 'opening_target' => $oBrick->GetOpeningTarget(), + ); + } + } + } + elseif ($oAttDef instanceof AttributeSubItem || $oAttDef instanceof AttributeDuration) + { + $sValue = $oAttDef->GetAsHTML($oCurrentRow->Get($sItemAttr)); + } + else + { + $sValue = $oAttDef->GetValueLabel($oCurrentRow->Get($sItemAttr)); + } + unset($oAttDef); + + $aItemAttrs[$sItemAttr] = array( + 'att_code' => $sItemAttr, + 'value' => $sValue, + 'actions' => $aActions + ); + } + + // ... Checking menu extensions + $aItemButtons = array(); + foreach (MetaModel::EnumPlugins('iPopupMenuExtension') as $oExtensionInstance) + { + foreach ($oExtensionInstance->EnumItems(iPopupMenuExtension::PORTAL_OBJLISTITEM_ACTIONS, array( + 'portal_id' => $oApp['combodo.portal.instance.id'], + 'object' => $oCurrentRow + )) as $oMenuItem) + { + if (is_object($oMenuItem)) + { + if ($oMenuItem instanceof JSButtonItem) + { + $aItemButtons[] = $oMenuItem->GetMenuItem() + array( + 'js_files' => $oMenuItem->GetLinkedScripts(), + 'type' => 'button' + ); + } + elseif ($oMenuItem instanceof URLButtonItem) + { + $aItemButtons[] = $oMenuItem->GetMenuItem() + array('type' => 'link'); + } + } + } + } + + // ... And item's properties + $aItems[] = array( + 'id' => $oCurrentRow->GetKey(), + 'class' => $sCurrentClass, + 'attributes' => $aItemAttrs, + 'highlight_class' => $oCurrentRow->GetHilightClass(), + 'actions' => $aItemButtons, + ); + + if (!empty($aItemButtons)) + { + $bHasObjectListItemExtension = true; + } + } + + // Adding an extra column for object list item extensions + if ($bHasObjectListItemExtension === true) + { + $aColumnsDefinition['_ui_extensions'] = array( + 'title' => Dict::S('Brick:Portal:Manage:Table:ItemActions'), + 'type' => 'html', + ); + } + + $aGroupingAreasData[$sKey] = array( + 'sId' => $sKey, + 'sTitle' => $aGroupingAreasValues[$sKey]['label'], + 'aItems' => $aItems, + 'iItemsCount' => $oSet->Count(), + 'aColumnsDefinition' => $aColumnsDefinition ); - - if(!empty($aItemButtons)) - { - $bHasObjectListItemExtension = true; - } } - - // Adding an extra column for object list item extensions - if($bHasObjectListItemExtension === true) - { - $aColumnsDefinition['_ui_extensions'] = array( - 'title' => Dict::S('Brick:Portal:Manage:Table:ItemActions'), - 'type' => 'html', - ); - } - - $aGroupingAreasData[$sKey] = array( - 'sId' => $sKey, - 'sTitle' => $aGroupingAreasValues[$sKey]['label'], - 'aItems' => $aItems, - 'iItemsCount' => $oSet->Count(), - 'aColumnsDefinition' => $aColumnsDefinition - ); + } + else + { + $aGroupingAreasData = array(); + $sGroupingArea = null; } // Preparing response if ($oRequest->isXmlHttpRequest()) { $aData = $aData + array( - 'data' => $aGroupingAreasData[$sGroupingArea]['aItems'] - ); - $oResponse = $oApp->json($aData); + 'data' => $aGroupingAreasData[$sGroupingArea]['aItems'] + ); } else { - $aData = $aData + array( - 'oBrick' => $oBrick, - 'sBrickId' => $sBrickId, - 'sGroupingTab' => $sGroupingTab, - 'aGroupingTabsValues' => $aGroupingTabsValues, - 'sDataLoading' => $sDataLoading, - 'aGroupingAreasData' => $aGroupingAreasData, - 'sDateFormat' => AttributeDate::GetFormat()->ToMomentJS(), - 'sDateTimeFormat' => AttributeDateTime::GetFormat()->ToMomentJS(), - 'sSearchValue' => $sSearchValue, - ); + $aDisplayValues = array(); + $aUrls = array(); + $aColumns = array(); + $aNames = array(); + if ($bHasScope) + { + foreach ($aGroupingTabsValues as $aValues) + { + $aDisplayValues[] = array( + 'value' => $aValues['count'], + 'label' => $aValues['label'], + 'label_html' => $aValues['label_html'], + ); + $aUrls[] = $oApp['url_generator']->generate('p_manage_brick', array( + 'sBrickId' => $sBrickId, + 'sDisplayType' => 'default', + 'sGroupingTab' => $aValues['value'] + )); + } - $oResponse = $oApp['twig']->render($oBrick->GetPageTemplatePath(), $aData); + foreach ($aDisplayValues as $idx => $aValue) + { + $aColumns[] = array('series_'.$idx, (int)$aValue['value']); + $aNames['series_'.$idx] = $aValue['label']; + } + } + + // Preparing data to pass to the templating service + $aData = $aData + array( + 'sFct' => 'count', + 'sIconURL' => $sIconURL, + 'aColumns' => $aColumns, + 'aNames' => $aNames, + 'aDisplayValues' => $aDisplayValues, + 'aUrls' => $aUrls, + 'oBrick' => $oBrick, + 'sBrickId' => $sBrickId, + 'sGroupingTab' => $sGroupingTab, + 'aGroupingTabsValues' => $aGroupingTabsValues, + 'sDataLoading' => $sDataLoading, + 'aGroupingAreasData' => $aGroupingAreasData, + 'sDateFormat' => AttributeDate::GetFormat()->ToMomentJS(), + 'sDateTimeFormat' => AttributeDateTime::GetFormat()->ToMomentJS(), + 'iCount' => $iCount, + ); } - return $oResponse; + return $aData; } + /** + * @param Request $oRequest + * @param array $aData + * @param DBSearch $oQuery + * @param string $sClass + */ + protected function ManageSearchValue(Request $oRequest, &$aData, DBSearch &$oQuery, $sClass) + { + // Getting search value + $sSearchValue = $oRequest->get('sSearchValue', null); + + // - Adding search clause if necessary + // Note : This is a very naive search at the moment + if ($sSearchValue !== null) + { + $aSearchListItems = MetaModel::GetZListItems($sClass, 'standard_search'); + $oFullBinExpr = null; + foreach ($aSearchListItems as $sSearchItemAttr) + { + $oBinExpr = new BinaryExpression(new FieldExpression($sSearchItemAttr, $oQuery->GetClassAlias()), + 'LIKE', new VariableExpression('search_value')); + // At each iteration we build the complete expression for the search like ( (field1 LIKE %search%) OR (field2 LIKE %search%) OR (field3 LIKE %search%) ...) + if (is_null($oFullBinExpr)) + { + $oFullBinExpr = $oBinExpr; + } + else + { + $oFullBinExpr = new BinaryExpression($oFullBinExpr, 'OR', $oBinExpr); + } + } + + // Then add the complete expression to the query + if (!is_null($oFullBinExpr)) + { + // - Adding expression to the query + $oQuery->AddConditionExpression($oFullBinExpr); + // - Setting expression parameters + // Note : This could be way more simpler if we had a SetInternalParam($sParam, $value) verb + $aQueryParams = $oQuery->GetInternalParams(); + $aQueryParams['search_value'] = '%'.$sSearchValue.'%'; + $oQuery->SetInternalParams($aQueryParams); + } + } + + $aData['sSearchValue'] = $sSearchValue; + } + + /** + * Get the groups using a given attribute code. + * If a limit is given, the remaining groups are aggregated (groupby result and search request). + * + * @param DBSearch $oQuery Initial query + * @param string $sGroupingTabAttCode Attribute code to group by + * @param Application $oApp + * @param ManageBrick $oBrick + * + * @return array of results from the groupby request and the corrsponding search. + * @throws \DictExceptionMissingString + * @throws \MissingQueryArgument + * @throws \MySQLException + * @throws \OQLException + */ + protected function GroupByAttribute( + DBSearch $oQuery, $sGroupingTabAttCode, Application $oApp, ManageBrick $oBrick + ) { + $aGroupingTabsValues = array(); + $aDistinctResults = array(); + $oDistinctQuery = DBSearch::FromOQL($oBrick->GetOql()); + $bHasScope = $this->AddScopeToQuery($oDistinctQuery, $oApp, $oBrick, $oDistinctQuery->GetClass()); + if ($bHasScope) + { + // - Adding field condition + $oFieldExp = new FieldExpression($sGroupingTabAttCode, $oDistinctQuery->GetClassAlias()); + $sDistinctSql = $oDistinctQuery->MakeGroupByQuery(array(), array('grouped_by_1' => $oFieldExp), true); + $aDistinctResults = CMDBSource::QueryToArray($sDistinctSql); + if (!empty($aDistinctResults)) + { + $iLimit = $oBrick->GetGroupLimit(); + $aOthers = array(); + if ($iLimit > 0) + { + uasort($aDistinctResults, function ($a, $b) { + $v1 = $a['_itop_count_']; + $v2 = $b['_itop_count_']; + + return ($v1 == $v2) ? 0 : (($v1 > $v2) ? -1 : 1); + }); + + if (count($aDistinctResults) > $iLimit) + { + if ($oBrick->ShowGroupOthers()) + { + $aOthers = array_slice($aDistinctResults, $iLimit); + } + $aDistinctResults = array_slice($aDistinctResults, 0, $iLimit); + } + } + + foreach ($aDistinctResults as $aDistinctResult) + { + $oConditionQuery = DBSearch::CloneWithAlias($oQuery, 'GTAB'); + $oExpression = new BinaryExpression(new FieldExpression($sGroupingTabAttCode, + $oDistinctQuery->GetClassAlias()), '=', new UnaryExpression($aDistinctResult['grouped_by_1'])); + $oConditionQuery->AddConditionExpression($oExpression); + + $sHtmlLabel = $oFieldExp->MakeValueLabel($oDistinctQuery, $aDistinctResult['grouped_by_1'], ''); + $aGroupingTabsValues[$aDistinctResult['grouped_by_1']] = array( + 'value' => $aDistinctResult['grouped_by_1'], + 'label_html' => $sHtmlLabel, + 'label' => strip_tags($sHtmlLabel), + 'condition' => $oConditionQuery, + 'count' => $aDistinctResult['_itop_count_'], + ); + unset($oConditionQuery); + } + if (!empty($aOthers)) + { + // Aggregate others + $oConditionQuery = DBSearch::CloneWithAlias($oQuery, 'GTAB'); + $oExpression = null; + $iOtherCount = 0; + foreach ($aOthers as $aResult) + { + $iOtherCount += $aResult['_itop_count_']; + $oExpr = new BinaryExpression(new FieldExpression($sGroupingTabAttCode, + $oDistinctQuery->GetClassAlias()), '=', new UnaryExpression($aResult['grouped_by_1'])); + if (is_null($oExpression)) + { + $oExpression = $oExpr; + } + else + { + $oExpression = new BinaryExpression($oExpression, 'OR', $oExpr); + } + } + $oConditionQuery->AddConditionExpression($oExpression); + + $sLabel = Dict::S('Brick:Portal:Manage:Others'); + $aGroupingTabsValues['Others'] = array( + 'value' => 'Others', + 'label_html' => $sLabel, + 'label' => $sLabel, + 'condition' => $oConditionQuery, + 'count' => $iOtherCount, + ); + unset($oConditionQuery); + } + } + } + if (empty($aDistinctResults)) + { + $sLabel = Dict::S('Brick:Portal:Manage:All'); + $aGroupingTabsValues['undefined'] = array( + 'value' => 'All', + 'label_html' => $sLabel, + 'label' => $sLabel, + 'condition' => null, + 'count' => 0, + ); + } + + return $aGroupingTabsValues; + } + + /** + * @param Application $oApp + * @param ManageBrick $oBrick + * @param string $sClass + * + * @return DBSearch + * @throws \OQLException + */ + protected function GetScopedQuery(Application $oApp, ManageBrick $oBrick, $sClass) + { + $oQuery = DBSearch::FromOQL($oBrick->GetOql()); + $this->AddScopeToQuery($oQuery, $oApp, $oBrick, $sClass); + + return $oQuery; + } + + /** + * @param DBSearch $oQuery + * @param Application $oApp + * @param ManageBrick $oBrick + * @param string $sClass + * + * @return bool true if scope exists, false if scope is null + */ + protected function AddScopeToQuery(DBSearch &$oQuery, Application $oApp, ManageBrick $oBrick, $sClass) + { + $oScopeQuery = $oApp['scope_validator']->GetScopeFilterForProfiles(UserRights::ListProfiles(), $sClass, + UR_ACTION_READ); + if ($oScopeQuery !== null) + { + $oQuery = $oQuery->Intersect($oScopeQuery); + // - Allowing all data if necessary + if ($oScopeQuery->IsAllDataAllowed()) + { + $oQuery->AllowAllData(); + } + + return true; + } + + return false; + } } diff --git a/datamodels/2.x/itop-portal-base/portal/src/entities/managebrick.class.inc.php b/datamodels/2.x/itop-portal-base/portal/src/entities/managebrick.class.inc.php index 3f8a1f696..a7969245f 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/entities/managebrick.class.inc.php +++ b/datamodels/2.x/itop-portal-base/portal/src/entities/managebrick.class.inc.php @@ -4,7 +4,7 @@ // // This file is part of iTop. // -// iTop is free software; you can redistribute it and/or modify +// iTop is free software; you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. @@ -16,53 +16,97 @@ // // You should have received a copy of the GNU Affero General Public License // along with iTop. If not, see +// namespace Combodo\iTop\Portal\Brick; -use DOMFormatException; -use DBSearch; -use MetaModel; use Combodo\iTop\DesignElement; -use Combodo\iTop\Portal\Brick\PortalBrick; +use DBSearch; +use DOMFormatException; +use MetaModel; + +define('MANAGE_BRICK_LAYOUT_PATH', 'itop-portal-base/portal/src/views/bricks/manage/'); -/** - * Description of ManageBrick - * - * @author Guillaume Lajarige - */ class ManageBrick extends PortalBrick { - const ENUM_ACTION_VIEW = 'view'; - const ENUM_ACTION_EDIT = 'edit'; + const ENUM_ACTION_VIEW = 'view'; + const ENUM_ACTION_EDIT = 'edit'; const DEFAULT_DECORATION_CLASS_HOME = 'fa fa-pencil-square'; const DEFAULT_DECORATION_CLASS_NAVIGATION_MENU = 'fa fa-pencil-square fa-2x'; - const DEFAULT_PAGE_TEMPLATE_PATH = 'itop-portal-base/portal/src/views/bricks/manage/layout.html.twig'; + const DEFAULT_PAGE_TEMPLATE_PATH = 'itop-portal-base/portal/src/views/bricks/manage/layout-table.html.twig'; + const CHART_PAGE_TEMPLATE_PATH = 'itop-portal-base/portal/src/views/bricks/manage/layout-chart.html.twig'; const DEFAULT_OQL = ''; const DEFAULT_OPENING_MODE = self::ENUM_ACTION_EDIT; const DEFAULT_DATA_LOADING = self::ENUM_DATA_LOADING_LAZY; const DEFAULT_LIST_LENGTH = 20; const DEFAULT_ZLIST_FIELDS = 'list'; - const DEFAULT_SHOW_TAB_COUNTS = false; + const DEFAULT_SHOW_TAB_COUNTS = false; + + const DEFAULT_TILE_TEMPLATE_PATH = 'itop-portal-base/portal/src/views/bricks/manage/tile-default.html.twig'; + const DEFAULT_TILE_CONTROLLER_ACTION = 'Combodo\\iTop\\Portal\\Controller\\ManageBrickController::TileAction'; static $sRouteName = 'p_manage_brick'; protected $sOql; protected $sOpeningMode; protected $aGrouping; protected $aFields; + protected $aExportFields; protected $bShowTabCounts; + protected $sDisplayType; + protected $iGroupLimit; + protected $bGroupShowOthers; + + protected $aPresentationData = array( + 'badge' => array( + 'decorationCssClass' => 'fa fa-id-card-o fa-2x', + 'tileTemplate' => 'itop-portal-base/portal/src/views/bricks/manage/tile-badge.html.twig', + 'layoutTemplate' => 'itop-portal-base/portal/src/views/bricks/manage/layout-table.html.twig', + 'layoutDisplayType' => 'default', + 'need_details' => true, + ), + 'top-list' => array( + 'decorationCssClass' => 'fa fa-signal fa-rotate-270 fa-2x', + 'tileTemplate' => 'itop-portal-base/portal/src/views/bricks/manage/tile-top-list.html.twig', + 'layoutTemplate' => 'itop-portal-base/portal/src/views/bricks/manage/layout-table.html.twig', + 'layoutDisplayType' => 'default', + 'need_details' => true, + ), + 'pie-chart' => array( + 'decorationCssClass' => 'fa fa-pie-chart fa-2x', + 'tileTemplate' => 'itop-portal-base/portal/src/views/bricks/manage/tile-chart.html.twig', + 'layoutTemplate' => 'itop-portal-base/portal/src/views/bricks/manage/layout-chart.html.twig', + 'layoutDisplayType' => 'pie-chart', + 'need_details' => false, + ), + 'bar-chart' => array( + 'decorationCssClass' => 'fa fa-bar-chart fa-2x', + 'tileTemplate' => 'itop-portal-base/portal/src/views/bricks/manage/tile-chart.html.twig', + 'layoutTemplate' => 'itop-portal-base/portal/src/views/bricks/manage/layout-chart.html.twig', + 'layoutDisplayType' => 'bar-chart', + 'need_details' => false, + ), + 'default' => array( + 'decorationCssClass' => 'fa fa-pencil-square fa-2x', + 'tileTemplate' => self::DEFAULT_TILE_TEMPLATE_PATH, + 'layoutTemplate' => 'itop-portal-base/portal/src/views/bricks/manage/layout-table.html.twig', + 'layoutDisplayType' => 'default', + 'need_details' => true, + ), + ); public function __construct() { parent::__construct(); $this->sOql = static::DEFAULT_OQL; - $this->sOpeningMode = static::DEFAULT_OPENING_MODE; + $this->sOpeningMode = static::DEFAULT_OPENING_MODE; $this->aGrouping = array(); $this->aFields = array(); + $this->aExportFields = array(); $this->bShowTabCounts = static::DEFAULT_SHOW_TAB_COUNTS; - // This is hardcoded for now, we might allow area grouping on another attribute in the futur + // This is hardcoded for now, we might allow area grouping on another attribute in the future $this->AddGrouping('areas', array('attribute' => 'finalclass')); } @@ -76,15 +120,15 @@ class ManageBrick extends PortalBrick return $this->sOql; } - /** - * Returns the brick's objects opening mode (edit or view) - * - * @return string - */ - public function GetOpeningMode() - { - return $this->sOpeningMode; - } + /** + * Returns the brick's objects opening mode (edit or view) + * + * @return string + */ + public function GetOpeningMode() + { + return $this->sOpeningMode; + } /** * Returns the brick grouping @@ -106,39 +150,114 @@ class ManageBrick extends PortalBrick return $this->aFields; } - /** - * Returns if the brick should display objects count on tabs - * - * @return bool - */ + /** + * Returns the brick fields to export + * + * @return array + */ + public function GetExportFields() + { + return $this->aExportFields; + } + + /** + * Returns if the brick should display objects count on tabs + * + * @return bool + */ public function GetShowTabCounts() - { - return $this->bShowTabCounts; - } + { + return $this->bShowTabCounts; + } + + /** + * @return string + */ + public function GetDisplayType() + { + return $this->sDisplayType; + } + + /** + * @param string $sDisplayType + */ + public function SetDisplayType($sDisplayType) + { + $this->sDisplayType = $sDisplayType; + } + + /** + * @param string $sDisplayType + * + * @return string[] parameters for specified type, default parameters if type is invalid + */ + public function GetPresentationDataForDisplayType($sDisplayType) + { + if (isset($this->aPresentationData[$sDisplayType])) + { + return $this->aPresentationData[$sDisplayType]; + } + + return $this->aPresentationData['default']; + } + + /** + * @return mixed + */ + public function GetGroupLimit() + { + return $this->iGroupLimit; + } + + /** + * @return mixed + */ + public function ShowGroupOthers() + { + return $this->bGroupShowOthers; + } + + /** + * @return array + */ + public function ListLayoutDisplayTypes() + { + $aLayoutDisplayTypes = array(); + foreach ($this->aPresentationData as $aPresentationDatum) + { + $aLayoutDisplayTypes[$aPresentationDatum['layoutDisplayType']] = true; + } + + return array_keys($aLayoutDisplayTypes); + } /** * Sets the oql of the brick * * @param string $sOql - * @return \Combodo\iTop\Portal\Brick\ManageBrick + * + * @return ManageBrick */ public function SetOql($sOql) { $this->sOql = $sOql; + return $this; } - /** - * Sets the brick's objects opening mode - * - * @param string $sOpeningMode - * @return \Combodo\iTop\Portal\Brick\ManageBrick - */ - public function SetOpeningMode($sOpeningMode) - { - $this->sOpeningMode = $sOpeningMode; - return $this; - } + /** + * Sets the brick's objects opening mode + * + * @param string $sOpeningMode + * + * @return ManageBrick + */ + public function SetOpeningMode($sOpeningMode) + { + $this->sOpeningMode = $sOpeningMode; + + return $this; + } /** * Sets the grouping of the brick @@ -148,6 +267,7 @@ class ManageBrick extends PortalBrick public function SetGrouping($aGrouping) { $this->aGrouping = $aGrouping; + return $this; } @@ -159,20 +279,23 @@ class ManageBrick extends PortalBrick public function SetFields($aFields) { $this->aFields = $aFields; + return $this; } - /** - * Sets if the brick should display objects count on tab - * - * @param bool $bShowTabCounts - * @return \Combodo\iTop\Portal\Brick\ManageBrick - */ - public function SetShowTabCounts($bShowTabCounts) - { - $this->bShowTabCounts = $bShowTabCounts; - return $this; - } + /** + * Sets if the brick should display objects count on tab + * + * @param bool $bShowTabCounts + * + * @return ManageBrick + */ + public function SetShowTabCounts($bShowTabCounts) + { + $this->bShowTabCounts = $bShowTabCounts; + + return $this; + } /** * Adds a grouping. @@ -181,7 +304,8 @@ class ManageBrick extends PortalBrick * * @param string $sName (Must be "tabs" or -Not implemented yet, implicit grouping on y axis-) * @param array $aGrouping - * @return \Combodo\iTop\Portal\Brick\ManageBrick + * + * @return ManageBrick */ public function AddGrouping($sName, $aGrouping) { @@ -190,8 +314,7 @@ class ManageBrick extends PortalBrick // Sorting if (!$this->IsGroupingByDistinctValues($sName)) { - usort($this->aGrouping[$sName]['groups'], function($a, $b) - { + usort($this->aGrouping[$sName]['groups'], function ($a, $b) { return $a['rank'] > $b['rank']; }); } @@ -203,7 +326,8 @@ class ManageBrick extends PortalBrick * Removes a grouping by its name * * @param string $sName - * @return \Combodo\iTop\Portal\Brick\ManageBrick + * + * @return ManageBrick */ public function RemoveGrouping($sName) { @@ -211,6 +335,7 @@ class ManageBrick extends PortalBrick { unset($this->aGrouping[$sName]); } + return $this; } @@ -218,7 +343,8 @@ class ManageBrick extends PortalBrick * Adds a field to display from its attribute_code. * * @param string $sAttCode - * @return \Combodo\iTop\Portal\Brick\ManageBrick + * + * @return ManageBrick */ public function AddField($sAttCode) { @@ -234,7 +360,8 @@ class ManageBrick extends PortalBrick * Removes a field * * @param string $sAttCode - * @return \Combodo\iTop\Portal\Brick\ManageBrick + * + * @return ManageBrick */ public function RemoveField($sAttCode) { @@ -242,9 +369,31 @@ class ManageBrick extends PortalBrick { unset($this->aFields[$sAttCode]); } + return $this; } + public function AddExportField($sAttCode) + { + if (!in_array($sAttCode, $this->aExportFields)) + { + $this->aExportFields[] = $sAttCode; + } + + return $this; + } + + public function RemoveExportField($sAttCode) + { + if (isset($this->aExportFields[$sAttCode])) + { + unset($this->aExportFields[$sAttCode]); + } + + return $this; + } + + /** * Returns if the brick has grouping tabs or not. * @@ -290,6 +439,7 @@ class ManageBrick extends PortalBrick * This is supposed to be called by the IsGroupingTabsByDistinctValues / IsGroupingAreasByDistinctValues function. * * @param string $sGroupingName + * * @return boolean */ public function IsGroupingByDistinctValues($sGroupingName) @@ -299,7 +449,8 @@ class ManageBrick extends PortalBrick /** * Returns true is the groupings tabs properties exists and is of the form attribute => attribute_code. - * This is mostly used to know if the tabs are grouped by attribute distinct values or by meta-groups (eg : status in ('accepted', 'opened')). + * This is mostly used to know if the tabs are grouped by attribute distinct values or by meta-groups (eg : status + * in ('accepted', 'opened')). * * @return boolean */ @@ -310,7 +461,8 @@ class ManageBrick extends PortalBrick /** * Returns true is the groupings areas properties exists and is of the form attribute => attribute_code. - * This is mostly used to know if the areas are grouped by attribute distinct values or by meta-groups (eg : finalclass in ('Server', 'Router')). + * This is mostly used to know if the areas are grouped by attribute distinct values or by meta-groups (eg : + * finalclass in ('Server', 'Router')). * * @return boolean */ @@ -324,11 +476,18 @@ class ManageBrick extends PortalBrick * This is used to set all the brick attributes at once. * * @param \Combodo\iTop\DesignElement $oMDElement + * * @return ManageBrick + * @throws DOMFormatException + * @throws \OQLException */ public function LoadFromXml(DesignElement $oMDElement) { parent::LoadFromXml($oMDElement); + $this->sDisplayType = 'default'; + $this->iGroupLimit = 0; + $this->bGroupShowOthers = true; + $bUseListFieldsForExport = false; // Checking specific elements foreach ($oMDElement->GetNodes('./*') as $oBrickSubNode) @@ -339,53 +498,90 @@ class ManageBrick extends PortalBrick $sClass = $oBrickSubNode->GetText(); if ($sClass === '') { - throw new DOMFormatException('ManageBrick : class tag is empty. Must contain Classname', null, null, $oBrickSubNode); + throw new DOMFormatException('ManageBrick : class tag is empty. Must contain Classname', null, + null, $oBrickSubNode); } - $this->SetOql('SELECT ' . $sClass); + $this->SetOql('SELECT '.$sClass); break; case 'oql': $sOql = $oBrickSubNode->GetText(); if ($sOql === '') { - throw new DOMFormatException('ManageBrick : oql tag is empty. Must contain OQL statement', null, null, $oBrickSubNode); + throw new DOMFormatException('ManageBrick : oql tag is empty. Must contain OQL statement', null, + null, $oBrickSubNode); } $this->SetOql($sOql); break; - case 'opening_mode': - $sOpeningMode = $oBrickSubNode->GetText(static::DEFAULT_OPENING_MODE); - if (!in_array($sOpeningMode, array(static::ENUM_ACTION_VIEW, static::ENUM_ACTION_EDIT))) - { - throw new DOMFormatException('ManageBrick : opening_mode tag value must be edit|view ("' . $sOpeningMode . '" given)', null, null, $oBrickSubNode); - } + case 'opening_mode': + $sOpeningMode = $oBrickSubNode->GetText(static::DEFAULT_OPENING_MODE); + if (!in_array($sOpeningMode, array(static::ENUM_ACTION_VIEW, static::ENUM_ACTION_EDIT))) + { + throw new DOMFormatException('ManageBrick : opening_mode tag value must be edit|view ("'.$sOpeningMode.'" given)', + null, null, $oBrickSubNode); + } - $this->SetOpeningMode($sOpeningMode); - break; + $this->SetOpeningMode($sOpeningMode); + break; + + case 'display_type': + $this->sDisplayType = $oBrickSubNode->GetText(); + $aDisplayParameterForType = $this->GetPresentationDataForDisplayType($this->sDisplayType); + $this->SetTileTemplatePath($aDisplayParameterForType['tileTemplate']); + $this->SetPageTemplatePath($aDisplayParameterForType['layoutTemplate']); + break; case 'fields': foreach ($oBrickSubNode->GetNodes('./field') as $oFieldNode) { if (!$oFieldNode->hasAttribute('id')) { - throw new DOMFormatException('ManageBrick : Field must have a unique ID attribute', null, null, $oFieldNode); + throw new DOMFormatException('ManageBrick : Field must have a unique ID attribute', null, + null, $oFieldNode); } $this->AddField($oFieldNode->getAttribute('id')); } break; + case 'export': + foreach ($oBrickSubNode->GetNodes('./*') as $oExportNode) + { + switch ($oExportNode->nodeName) + { + case 'fields': + foreach ($oExportNode->GetNodes('./field') as $oFieldNode) + { + if (!$oFieldNode->hasAttribute('id')) + { + throw new DOMFormatException('ManageBrick : Field must have a unique ID attribute', + null, + null, $oFieldNode); + } + $this->AddExportField($oFieldNode->getAttribute('id')); + } + break; + + case 'export_default_fields': + $bUseListFieldsForExport = (strtolower($oExportNode->GetText()) === 'true' ? true : false); + break; + } + + } + break; + case 'grouping': // Tabs grouping foreach ($oBrickSubNode->GetNodes('./tabs/*') as $oGroupingNode) { switch ($oGroupingNode->nodeName) { - case 'show_tab_counts'; - $bShowTabCounts = ( $oGroupingNode->GetText(static::DEFAULT_SHOW_TAB_COUNTS) === 'true' ) ? true : false; - $this->SetShowTabCounts($bShowTabCounts); - break; + case 'show_tab_counts'; + $bShowTabCounts = ($oGroupingNode->GetText(static::DEFAULT_SHOW_TAB_COUNTS) === 'true') ? true : false; + $this->SetShowTabCounts($bShowTabCounts); + break; case 'attribute': $sAttribute = $oGroupingNode->GetText(); if ($sAttribute !== '') @@ -393,13 +589,24 @@ class ManageBrick extends PortalBrick $this->AddGrouping('tabs', array('attribute' => $sAttribute)); } break; + case 'limit': + $iLimit = $oGroupingNode->GetText(); + if (is_numeric($iLimit)) + { + $this->iGroupLimit = $iLimit; + } + break; + case 'show_others': + $this->bGroupShowOthers = ($oGroupingNode->GetText() === 'true') ? true : false; + break; case 'groups': $aGroups = array(); foreach ($oGroupingNode->GetNodes('./group') as $oGroupNode) { if (!$oGroupNode->hasAttribute('id')) { - throw new DOMFormatException('ManageBrick : Group must have a unique ID attribute', null, null, $oGroupNode); + throw new DOMFormatException('ManageBrick : Group must have a unique ID attribute', + null, null, $oGroupNode); } $sGroupId = $oGroupNode->getAttribute('id'); @@ -410,7 +617,7 @@ class ManageBrick extends PortalBrick switch ($oGroupProperty->nodeName) { case 'rank': - $aGroup[$oGroupProperty->nodeName] = (int) $oGroupProperty->GetText(0); + $aGroup[$oGroupProperty->nodeName] = (int)$oGroupProperty->GetText(0); break; case 'title': case 'condition': @@ -422,11 +629,13 @@ class ManageBrick extends PortalBrick // Checking constitancy if (!isset($aGroup['title']) || $aGroup['title'] === '') { - throw new DOMFormatException('ManageBrick : Group must have a title tag and it must not be empty', null, null, $oGroupNode); + throw new DOMFormatException('ManageBrick : Group must have a title tag and it must not be empty', + null, null, $oGroupNode); } if (!isset($aGroup['condition']) || $aGroup['condition'] === '') { - throw new DOMFormatException('ManageBrick : Group must have a condition tag and it must not be empty', null, null, $oGroupNode); + throw new DOMFormatException('ManageBrick : Group must have a condition tag and it must not be empty', + null, null, $oGroupNode); } $aGroups[] = $aGroup; } @@ -448,11 +657,42 @@ class ManageBrick extends PortalBrick if (empty($this->aFields)) { $sClass = DBSearch::FromOQL($this->GetOql()); - $aFields = MetaModel::FlattenZList(MetaModel::GetZListItems($sClass->GetClass(), static::DEFAULT_ZLIST_FIELDS)); + $aFields = MetaModel::FlattenZList(MetaModel::GetZListItems($sClass->GetClass(), + static::DEFAULT_ZLIST_FIELDS)); $this->SetFields($aFields); } + // Default Export Fields + if ($bUseListFieldsForExport) + { + foreach ($this->GetFields() as $sAttCode) + { + $this->AddExportField($sAttCode); + } + } + + // Checking the navigation icon + $sDecorationClassNavigationMenu = $this->GetDecorationClassNavigationMenu(); + if (empty($sDecorationClassNavigationMenu) && isset($this->aPresentationData[$this->sDisplayType])) + { + $sDecorationClassNavigationMenu = $this->aPresentationData[$this->sDisplayType]['decorationCssClass']; + if (!empty($sDecorationClassNavigationMenu)) + { + $this->SetDecorationClassNavigationMenu($sDecorationClassNavigationMenu); + } + } + + $sTitle = $this->GetTitleHome(); + if (empty($sTitle)) + { + $sOql = $this->GetOql(); + $oSeach = DBSearch::FromOQL($sOql); + $sClassName = MetaModel::GetName($oSeach->GetClass()); + $this->SetTitleHome($sClassName); + $this->SetTitleNavigationMenu($sClassName); + $this->SetTitle($sClassName); + } + return $this; } - } diff --git a/datamodels/2.x/itop-portal-base/portal/src/helpers/applicationhelper.class.inc.php b/datamodels/2.x/itop-portal-base/portal/src/helpers/applicationhelper.class.inc.php index 493f9666f..4f865936e 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/helpers/applicationhelper.class.inc.php +++ b/datamodels/2.x/itop-portal-base/portal/src/helpers/applicationhelper.class.inc.php @@ -19,7 +19,17 @@ namespace Combodo\iTop\Portal\Helper; +use ApplicationContext; +use Combodo\iTop\Portal\Brick\AbstractBrick; +use DBObjectSearch; +use DBObjectSet; +use Dict; +use DOMFormatException; use Exception; +use iPortalUIExtension; +use IssueLog; +use MetaModel; +use ModuleDesign; use Silex\Application; use Symfony\Component\Debug\ErrorHandler; use Symfony\Component\Debug\ExceptionHandler; @@ -27,18 +37,8 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\HttpException; use Twig_Environment; use Twig_SimpleFilter; -use Dict; -use utils; -use IssueLog; use UserRights; -use DOMFormatException; -use ModuleDesign; -use MetaModel; -use DBObjectSearch; -use DBObjectSet; -use iPortalUIExtension; -use ApplicationContext; -use Combodo\iTop\Portal\Brick\AbstractBrick; +use utils; /** * Contains static methods to help loading / registering classes of the application. @@ -48,10 +48,10 @@ use Combodo\iTop\Portal\Brick\AbstractBrick; */ class ApplicationHelper { - const FORM_ENUM_DISPLAY_MODE_COSY = 'cosy'; - const FORM_ENUM_DISPLAY_MODE_COMPACT = 'compact'; - const FORM_DEFAULT_DISPLAY_MODE = self::FORM_ENUM_DISPLAY_MODE_COSY; - const FORM_DEFAULT_ALWAYS_SHOW_SUBMIT = false; + const FORM_ENUM_DISPLAY_MODE_COSY = 'cosy'; + const FORM_ENUM_DISPLAY_MODE_COMPACT = 'compact'; + const FORM_DEFAULT_DISPLAY_MODE = self::FORM_ENUM_DISPLAY_MODE_COSY; + const FORM_DEFAULT_ALWAYS_SHOW_SUBMIT = false; /** * Loads classes from the base portal @@ -59,6 +59,7 @@ class ApplicationHelper * @param string $sScannedDir Directory to load the files from * @param string $sFilePattern Pattern of files to load * @param string $sType Type of files to load, used only in the Exception message, can be anything + * * @throws \Exception */ public static function LoadClasses($sScannedDir, $sFilePattern, $sType) @@ -66,7 +67,7 @@ class ApplicationHelper // Loading classes from base portal foreach (scandir($sScannedDir) as $sFile) { - if (strpos($sFile, $sFilePattern) !== false && file_exists($sFilepath = $sScannedDir . '/' . $sFile)) + if (strpos($sFile, $sFilePattern) !== false && file_exists($sFilepath = $sScannedDir.'/'.$sFile)) { try { @@ -74,7 +75,7 @@ class ApplicationHelper } catch (Exception $e) { - throw new Exception('Error while trying to load ' . $sType . ' ' . $sFile); + throw new Exception('Error while trying to load '.$sType.' '.$sFile); } } } @@ -84,13 +85,14 @@ class ApplicationHelper * Loads controllers from the base portal * * @param string $sScannedDir Directory to load the controllers from + * * @throws \Exception */ public static function LoadControllers($sScannedDir = null) { if ($sScannedDir === null) { - $sScannedDir = __DIR__ . '/../controllers'; + $sScannedDir = __DIR__.'/../controllers'; } // Loading controllers from base portal (those from modules have already been loaded by module.xxx.php files) @@ -101,13 +103,14 @@ class ApplicationHelper * Loads routers from the base portal * * @param string $sScannedDir Directory to load the routers from + * * @throws \Exception */ public static function LoadRouters($sScannedDir = null) { if ($sScannedDir === null) { - $sScannedDir = __DIR__ . '/../routers'; + $sScannedDir = __DIR__.'/../routers'; } // Loading routers from base portal (those from modules have already been loaded by module.xxx.php files) @@ -118,13 +121,14 @@ class ApplicationHelper * Loads bricks from the base portal * * @param string $sScannedDir Directory to load the bricks from + * * @throws \Exception */ public static function LoadBricks($sScannedDir = null) { if ($sScannedDir === null) { - $sScannedDir = __DIR__ . '/../entities'; + $sScannedDir = __DIR__.'/../entities'; } // Loading bricks from base portal (those from modules have already been loaded by module.xxx.php files) @@ -135,13 +139,14 @@ class ApplicationHelper * Loads form managers from the base portal * * @param string $sScannedDir Directory to load the managers from + * * @throws \Exception */ public static function LoadFormManagers($sScannedDir = null) { if ($sScannedDir === null) { - $sScannedDir = __DIR__ . '/../forms'; + $sScannedDir = __DIR__.'/../forms'; } // Loading form managers from base portal (those from modules have already been loaded by module.xxx.php files) @@ -152,6 +157,7 @@ class ApplicationHelper * Registers routes in the Silex Application from all declared Router classes * * @param \Silex\Application $oApp + * * @throws \Exception */ public static function RegisterRoutes(Application $oApp) @@ -188,6 +194,7 @@ class ApplicationHelper * * @param \Silex\Application $oApp * @param boolean $bNamesOnly If set to true, function will return only the routes' names, not the objects + * * @return array */ public static function GetRoutes(Application $oApp, $bNamesOnly = false) @@ -205,63 +212,61 @@ class ApplicationHelper { // Filter to translate a string via the Dict::S function // Usage in twig : {{ 'String:ToTranslate'|dict_s }} - $oTwigEnv->addFilter(new Twig_SimpleFilter('dict_s', function($sStringCode, $sDefault = null, $bUserLanguageOnly = false) - { - return Dict::S($sStringCode, $sDefault, $bUserLanguageOnly); - }) + $oTwigEnv->addFilter(new Twig_SimpleFilter('dict_s', + function ($sStringCode, $sDefault = null, $bUserLanguageOnly = false) { + return Dict::S($sStringCode, $sDefault, $bUserLanguageOnly); + }) ); // Filter to format a string via the Dict::Format function // Usage in twig : {{ 'String:ToTranslate'|dict_format() }} - $oTwigEnv->addFilter(new Twig_SimpleFilter('dict_format', function($sStringCode, $sParam01 = null, $sParam02 = null, $sParam03 = null, $sParam04 = null) - { - return Dict::Format($sStringCode, $sParam01, $sParam02, $sParam03, $sParam04); - }) + $oTwigEnv->addFilter(new Twig_SimpleFilter('dict_format', + function ($sStringCode, $sParam01 = null, $sParam02 = null, $sParam03 = null, $sParam04 = null) { + return Dict::Format($sStringCode, $sParam01, $sParam02, $sParam03, $sParam04); + }) ); // Filter to enable base64 encode/decode // Usage in twig : {{ 'String to encode'|base64_encode }} - $oTwigEnv->addFilter(new Twig_SimpleFilter('base64_encode', 'base64_encode')); - $oTwigEnv->addFilter(new Twig_SimpleFilter('base64_decode', 'base64_decode')); + $oTwigEnv->addFilter(new Twig_SimpleFilter('base64_encode', 'base64_encode')); + $oTwigEnv->addFilter(new Twig_SimpleFilter('base64_decode', 'base64_decode')); // Filter to enable json decode (encode already exists) // Usage in twig : {{ aSomeArray|json_decode }} - $oTwigEnv->addFilter(new Twig_SimpleFilter('json_decode', function($sJsonString, $bAssoc = false) - { - return json_decode($sJsonString, $bAssoc); - }) + $oTwigEnv->addFilter(new Twig_SimpleFilter('json_decode', function ($sJsonString, $bAssoc = false) { + return json_decode($sJsonString, $bAssoc); + }) ); // Filter to add itopversion to an url - $oTwigEnv->addFilter(new Twig_SimpleFilter('add_itop_version', function($sUrl) - { + $oTwigEnv->addFilter(new Twig_SimpleFilter('add_itop_version', function ($sUrl) { if (strpos($sUrl, '?') === false) { - $sUrl = $sUrl . "?itopversion=" . ITOP_VERSION; + $sUrl = $sUrl."?itopversion=".ITOP_VERSION; } else { - $sUrl = $sUrl . "&itopversion=" . ITOP_VERSION; + $sUrl = $sUrl."&itopversion=".ITOP_VERSION; } return $sUrl; })); - // Filter to add a module's version to an url - $oTwigEnv->addFilter(new Twig_SimpleFilter('add_module_version', function($sUrl, $sModuleName){ - $sModuleVersion = utils::GetCompiledModuleVersion($sModuleName); + // Filter to add a module's version to an url + $oTwigEnv->addFilter(new Twig_SimpleFilter('add_module_version', function ($sUrl, $sModuleName) { + $sModuleVersion = utils::GetCompiledModuleVersion($sModuleName); - if (strpos($sUrl, '?') === false) - { - $sUrl = $sUrl . "?moduleversion=" . $sModuleVersion; - } - else - { - $sUrl = $sUrl . "&moduleversion=" . $sModuleVersion; - } + if (strpos($sUrl, '?') === false) + { + $sUrl = $sUrl."?moduleversion=".$sModuleVersion; + } + else + { + $sUrl = $sUrl."&moduleversion=".$sModuleVersion; + } - return $sUrl; - })); + return $sUrl; + })); } /** @@ -272,15 +277,14 @@ class ApplicationHelper */ public static function RegisterExceptionHandler(Application $oApp) { - // Intercepting fatal errors and exceptions + // Intercepting fatal errors and exceptions ErrorHandler::register(); ExceptionHandler::register(($oApp['debug'] === true)); // Intercepting manually aborted request if (1 || !$oApp['debug']) { - $oApp->error(function(Exception $oException, Request $oRequest) use ($oApp) - { + $oApp->error(function (Exception $oException, Request $oRequest) use ($oApp) { $iErrorCode = ($oException instanceof HttpException) ? $oException->getStatusCode() : 500; $aData = array( @@ -300,7 +304,7 @@ class ApplicationHelper break; } - IssueLog::Error($aData['error_title'] . ' : ' . $aData['error_message']); + IssueLog::Error($aData['error_title'].' : '.$aData['error_message']); if ($oApp['request_stack']->getCurrentRequest()->isXmlHttpRequest()) { @@ -308,67 +312,69 @@ class ApplicationHelper } else { - // Preparing debug trace - $aSteps = array(); - foreach($oException->getTrace() as $aStep) - { - // - Default file name - if(!isset($aStep['file'])) - { - $aStep['file'] = ''; - } - $aFileParts = explode('\\', $aStep['file']); - // - Default line number - if(!isset($aStep['line'])) - { - $aStep['line'] = 'unknown'; - } - // - Default class name - if(isset($aStep['class']) && isset($aStep['function']) && isset($aStep['type'])) - { - $aClassParts = explode('\\', $aStep['class']); - $sClassName = $aClassParts[count($aClassParts)-1]; - $sClassFQ = $aStep['class']; + // Preparing debug trace + $aSteps = array(); + foreach ($oException->getTrace() as $aStep) + { + // - Default file name + if (!isset($aStep['file'])) + { + $aStep['file'] = ''; + } + $aFileParts = explode('\\', $aStep['file']); + // - Default line number + if (!isset($aStep['line'])) + { + $aStep['line'] = 'unknown'; + } + // - Default class name + if (isset($aStep['class']) && isset($aStep['function']) && isset($aStep['type'])) + { + $aClassParts = explode('\\', $aStep['class']); + $sClassName = $aClassParts[count($aClassParts) - 1]; + $sClassFQ = $aStep['class']; - $aArgsAsString = array(); - foreach($aStep['args'] as $arg) - { - if(is_array($arg)) - { - $aArgsAsString[] = 'array(...)'; - } - elseif(is_object($arg)) - { - $aArgsAsString[] = 'object('.get_class($arg).')'; - } - else - { - $aArgsAsString[] = $arg; - } - } + $aArgsAsString = array(); + foreach ($aStep['args'] as $arg) + { + if (is_array($arg)) + { + $aArgsAsString[] = 'array(...)'; + } + elseif (is_object($arg)) + { + $aArgsAsString[] = 'object('.get_class($arg).')'; + } + else + { + $aArgsAsString[] = $arg; + } + } - $sFunctionCall = $sClassName . $aStep['type'] . $aStep['function'] . '(' . implode(', ', $aArgsAsString) . ')'; - } - else - { - $sClassName = null; - $sClassFQ = null; - $sFunctionCall = null; - } + $sFunctionCall = $sClassName.$aStep['type'].$aStep['function'].'('.implode(', ', + $aArgsAsString).')'; + } + else + { + $sClassName = null; + $sClassFQ = null; + $sFunctionCall = null; + } - $aSteps[] = array( - 'file_fq' => $aStep['file'], - 'file_name' => $aFileParts[count($aFileParts)-1], - 'line' => $aStep['line'], - 'class_name' => $sClassName, - 'class_fq' => $sClassFQ, - 'function_call' => $sFunctionCall, - ); - } + $aSteps[] = array( + 'file_fq' => $aStep['file'], + 'file_name' => $aFileParts[count($aFileParts) - 1], + 'line' => $aStep['line'], + 'class_name' => $sClassName, + 'class_fq' => $sClassFQ, + 'function_call' => $sFunctionCall, + ); + } - $aData['debug_trace_steps'] = $aSteps; + $aData['debug_trace_steps'] = $aSteps; - $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/errors/layout.html.twig', $aData); + $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/errors/layout.html.twig', + $aData); } return $oResponse; @@ -380,6 +386,7 @@ class ApplicationHelper * Loads the portal instance configuration from its module design into the Silex application * * @param \Silex\Application $oApp + * * @throws Exception */ public static function LoadPortalConfiguration(Application $oApp) @@ -392,14 +399,14 @@ class ApplicationHelper throw new Exception('Cannot load module design, Portal ID is not defined'); } $oDesign = new ModuleDesign(PORTAL_ID); - + // Parsing file // - Default values $aPortalConf = array( 'properties' => array( 'id' => PORTAL_ID, 'name' => 'Page:DefaultTitle', - 'logo' => (file_exists(MODULESROOT . 'branding/portal-logo.png')) ? utils::GetAbsoluteUrlModulesRoot() . 'branding/portal-logo.png' : '../images/logo-itop-dark-bg.svg', + 'logo' => (file_exists(MODULESROOT.'branding/portal-logo.png')) ? utils::GetAbsoluteUrlModulesRoot().'branding/portal-logo.png' : '../images/logo-itop-dark-bg.svg', 'themes' => array( 'bootstrap' => 'itop-portal-base/portal/web/css/bootstrap-theme-combodo.scss', 'portal' => 'itop-portal-base/portal/web/css/portal.scss', @@ -414,30 +421,29 @@ class ApplicationHelper 'attachments' => array( 'allow_delete' => true ), - 'allowed_portals' => array( - 'opening_mode' => null, - ), + 'allowed_portals' => array( + 'opening_mode' => null, + ), ), 'portals' => array(), 'forms' => array(), - 'ui_extensions' => array( - 'css_files' => array(), - 'css_inline' => null, - 'js_files' => array(), - 'js_inline' => null, - 'html' => array(), - ), + 'ui_extensions' => array( + 'css_files' => array(), + 'css_inline' => null, + 'js_files' => array(), + 'js_inline' => null, + 'html' => array(), + ), 'bricks' => array(), 'bricks_total_width' => 0, ); // - Global portal properties foreach ($oDesign->GetNodes('/module_design/properties/*') as $oPropertyNode) { - $bPropertyNodeError = false; switch ($oPropertyNode->nodeName) { case 'name': - case 'urlmaker_class': + case 'urlmaker_class': case 'triggers_query': $aPortalConf['properties'][$oPropertyNode->nodeName] = $oPropertyNode->GetText($aPortalConf['properties'][$oPropertyNode->nodeName]); break; @@ -450,7 +456,8 @@ class ApplicationHelper { if (!$oSubNode->hasAttribute('id') || $oSubNode->GetText(null) === null) { - throw new DOMFormatException('Tag ' . $oSubNode->nodeName . ' must have a "id" attribute as well as a value', null, null, $oSubNode); + throw new DOMFormatException('Tag '.$oSubNode->nodeName.' must have a "id" attribute as well as a value', + null, null, $oSubNode); } $sNodeId = $oSubNode->getAttribute('id'); @@ -477,7 +484,8 @@ class ApplicationHelper $aPortalConf['properties']['templates'][$sNodeId] = $oSubNode->GetText(null); break; default: - throw new DOMFormatException('Value "' . $sNodeId . '" is not handled for template[@id]', null, null, $oSubNode); + throw new DOMFormatException('Value "'.$sNodeId.'" is not handled for template[@id]', + null, null, $oSubNode); break; } break; @@ -501,23 +509,23 @@ class ApplicationHelper } } break; - case 'allowed_portals': - foreach ($oPropertyNode->GetNodes('*') as $oSubNode) - { - switch ($oSubNode->nodeName) - { - case 'opening_mode': - $sValue = $oSubNode->GetText(); - // If the text is null, we keep the default value - // Else we set it - if ($sValue !== null) - { - $aPortalConf['properties']['allowed_portals'][$oSubNode->nodeName] = ($sValue === 'self') ? 'self' : 'tab'; - } - break; - } - } - break; + case 'allowed_portals': + foreach ($oPropertyNode->GetNodes('*') as $oSubNode) + { + switch ($oSubNode->nodeName) + { + case 'opening_mode': + $sValue = $oSubNode->GetText(); + // If the text is null, we keep the default value + // Else we set it + if ($sValue !== null) + { + $aPortalConf['properties']['allowed_portals'][$oSubNode->nodeName] = ($sValue === 'self') ? 'self' : 'tab'; + } + break; + } + } + break; } } // - Rectifying portal logo url @@ -525,7 +533,7 @@ class ApplicationHelper if (!preg_match('/^http/', $sLogoUri)) { // We prefix it with the server base url - $sLogoUri = utils::GetAbsoluteUrlAppRoot() . 'env-' . utils::GetCurrentEnvironment() . '/' . $sLogoUri; + $sLogoUri = utils::GetAbsoluteUrlAppRoot().'env-'.utils::GetCurrentEnvironment().'/'.$sLogoUri; } $aPortalConf['properties']['logo'] = $sLogoUri; // - User allowed portals @@ -537,32 +545,34 @@ class ApplicationHelper // - Scopes static::LoadScopesConfiguration($oApp, $oDesign); // - Lifecycle - static::LoadLifecycleConfiguration($oApp, $oDesign); + static::LoadLifecycleConfiguration($oApp, $oDesign); // - Presentation lists $aPortalConf['lists'] = static::LoadListsConfiguration($oApp, $oDesign); // - UI extensions - $aPortalConf['ui_extensions'] = static::LoadUIExtensions($oApp); + $aPortalConf['ui_extensions'] = static::LoadUIExtensions($oApp); // - Action rules static::LoadActionRulesConfiguration($oApp, $oDesign); // - Setting UrlMakerClass - if($aPortalConf['properties']['urlmaker_class'] !== null) - { - ApplicationContext::SetUrlMakerClass($aPortalConf['properties']['urlmaker_class']); - } + if ($aPortalConf['properties']['urlmaker_class'] !== null) + { + ApplicationContext::SetUrlMakerClass($aPortalConf['properties']['urlmaker_class']); + } // - Generating CSS files - $aImportPaths = array($oApp['combodo.portal.base.absolute_path'] . 'css/'); + $aImportPaths = array($oApp['combodo.portal.base.absolute_path'].'css/'); foreach ($aPortalConf['properties']['themes'] as $key => $value) { if (!is_array($value)) { - $aPortalConf['properties']['themes'][$key] = $oApp['combodo.absolute_url'] . utils::GetCSSFromSASS('env-' . utils::GetCurrentEnvironment() . '/' . $value, $aImportPaths); + $aPortalConf['properties']['themes'][$key] = $oApp['combodo.absolute_url'].utils::GetCSSFromSASS('env-'.utils::GetCurrentEnvironment().'/'.$value, + $aImportPaths); } else { $aValues = array(); foreach ($value as $sSubvalue) { - $aValues[] = $oApp['combodo.absolute_url'] . utils::GetCSSFromSASS('env-' . utils::GetCurrentEnvironment() . '/' . $sSubvalue, $aImportPaths); + $aValues[] = $oApp['combodo.absolute_url'].utils::GetCSSFromSASS('env-'.utils::GetCurrentEnvironment().'/'.$sSubvalue, + $aImportPaths); } $aPortalConf['properties']['themes'][$key] = $aValues; } @@ -572,7 +582,7 @@ class ApplicationHelper } catch (Exception $e) { - throw new Exception('Error while parsing portal configuration file : ' . $e->getMessage()); + throw new Exception('Error while parsing portal configuration file : '.$e->getMessage()); } } @@ -580,6 +590,7 @@ class ApplicationHelper * Loads the current user and stores it in the Silex application so we can use it wherever in the application * * @param \Silex\Application $oApp + * * @throws Exception */ public static function LoadCurrentUser(Application $oApp) @@ -594,23 +605,25 @@ class ApplicationHelper $oApp['combodo.current_user'] = $oUser; // Contact - $sContactPhotoUrl = $oApp['combodo.portal.base.absolute_url'] . 'img/user-profile-default-256px.png'; + $sContactPhotoUrl = $oApp['combodo.portal.base.absolute_url'].'img/user-profile-default-256px.png'; // - Checking if we can load the contact - try{ - $oContact = UserRights::GetContactObject(); - } - catch(Exception $e) - { - $oAllowedOrgSet = $oUser->Get('allowed_org_list'); - if($oAllowedOrgSet->Count() > 0) - { - throw new Exception('Could not load contact related to connected user. (Tip: Make sure the contact\'s organization is among the user\'s allowed organizations)'); - } - else{ - throw new Exception('Could not load contact related to connected user.'); - } - } - // - Retrieving picture + try + { + $oContact = UserRights::GetContactObject(); + } + catch (Exception $e) + { + $oAllowedOrgSet = $oUser->Get('allowed_org_list'); + if ($oAllowedOrgSet->Count() > 0) + { + throw new Exception('Could not load contact related to connected user. (Tip: Make sure the contact\'s organization is among the user\'s allowed organizations)'); + } + else + { + throw new Exception('Could not load contact related to connected user.'); + } + } + // - Retrieving picture if ($oContact) { if (MetaModel::IsValidAttCode(get_class($oContact), 'picture')) @@ -622,7 +635,8 @@ class ApplicationHelper } else { - $sContactPhotoUrl = MetaModel::GetAttributeDef(get_class($oContact), 'picture')->Get('default_image'); + $sContactPhotoUrl = MetaModel::GetAttributeDef(get_class($oContact), + 'picture')->Get('default_image'); } } } @@ -632,7 +646,9 @@ class ApplicationHelper /** * Loads the brick's security from the OQL queries to profiles arrays * - * @param \Combodo\iTop\Portal\Helper\AbstractBrick $oBrick + * @param \Combodo\iTop\Portal\Brick\AbstractBrick $oBrick + * + * @throws \Exception */ public static function LoadBrickSecurity(AbstractBrick &$oBrick) { @@ -662,7 +678,7 @@ class ApplicationHelper } catch (Exception $e) { - throw new Exception('Error while loading security from ' . $oBrick->GetId() . ' brick'); + throw new Exception('Error while loading security from '.$oBrick->GetId().' brick'); } } @@ -671,6 +687,7 @@ class ApplicationHelper * * @param \Silex\Application $oApp * @param string $sBrickId + * * @return \Combodo\iTop\Portal\Brick\AbstractBrick * @throws Exception */ @@ -689,7 +706,7 @@ class ApplicationHelper if (!$bFound) { - throw new Exception('Brick with id = "' . $sBrickId . '" was not found among loaded bricks.'); + throw new Exception('Brick with id = "'.$sBrickId.'" was not found among loaded bricks.'); } return $oBrick; @@ -704,7 +721,9 @@ class ApplicationHelper * @param Application $oApp * @param string $sClass Object class to find a form for * @param string $sMode Form mode to find (view|edit|create) + * * @return array + * @throws \CoreException */ public static function GetLoadedFormFromClass(Application $oApp, $sClass, $sMode) { @@ -753,7 +772,9 @@ class ApplicationHelper * @param Application $oApp * @param string $sClass Object class to find a list for * @param string $sList List name to find + * * @return array Array of attribute codes + * @throws \CoreException */ public static function GetLoadedListFromClass(Application $oApp, $sClass, $sList = 'default') { @@ -774,35 +795,21 @@ class ApplicationHelper // If not found, we try find one from the closest parent class else { - $bFound = false; foreach (MetaModel::EnumParentClasses($sClass) as $sParentClass) { // Trying to find the right list if (isset($aLists[$sParentClass]) && isset($aLists[$sParentClass][$sList])) { $aList = $aLists[$sParentClass][$sList]; - $bFound = true; break; } // Or the default list elseif (isset($aLists[$sParentClass]) && isset($aLists[$sParentClass]['default'])) { $aList = $aLists[$sParentClass]['default']; - $bFound = true; break; } } - - // If we have still not found one, we return a default form - if (!$bFound) - { - $aForm = array( - 'id' => 'default', - 'type' => 'zlist', - 'fields' => 'details', - 'layout' => null - ); - } } // If found, we flatten the list to keep only the attribute codes (not the rank) @@ -828,9 +835,9 @@ class ApplicationHelper * * @param \Silex\Application $oApp * @param ModuleDesign $oDesign + * * @return array * @throws Exception - * @throws DOMFormatException */ protected static function LoadBricksConfiguration(Application $oApp, ModuleDesign $oDesign) { @@ -886,30 +893,29 @@ class ApplicationHelper } else { - throw new DOMFormatException('Unknown brick class "' . $sBrickClass . '" from xsi:type attribute', null, null, $oBrickNode); + throw new DOMFormatException('Unknown brick class "'.$sBrickClass.'" from xsi:type attribute', null, + null, $oBrickNode); } } catch (DOMFormatException $e) { - throw new Exception('Could not create brick (' . $sBrickClass . ') from XML because of a DOM problem : ' . $e->getMessage()); + throw new Exception('Could not create brick ('.$sBrickClass.') from XML because of a DOM problem : '.$e->getMessage()); } catch (Exception $e) { - throw new Exception('Could not create brick (' . $sBrickClass . ') from XML : ' . $oBrickNode->Dump() . ' ' . $e->getMessage()); + throw new Exception('Could not create brick ('.$sBrickClass.') from XML : '.$oBrickNode->Dump().' '.$e->getMessage()); } } // - Sorting bricks by rank $aPortalConf['bricks_ordering'] = array(); // - Home $aPortalConf['bricks_ordering']['home'] = $aPortalConf['bricks']; - usort($aPortalConf['bricks_ordering']['home'], function($a, $b) - { + usort($aPortalConf['bricks_ordering']['home'], function ($a, $b) { return $a->GetRankHome() > $b->GetRankHome(); }); // - Navigation menu $aPortalConf['bricks_ordering']['navigation_menu'] = $aPortalConf['bricks']; - usort($aPortalConf['bricks_ordering']['navigation_menu'], function($a, $b) - { + usort($aPortalConf['bricks_ordering']['navigation_menu'], function ($a, $b) { return $a->GetRankNavigationMenu() > $b->GetRankNavigationMenu(); }); @@ -919,16 +925,17 @@ class ApplicationHelper /** * Loads the forms configuration from the module design XML and returns it as an array containing : * - => array( - * 'view'|'edit'|'create' => array( - * 'fields_type' => 'custom_list'|'twig'|'zlist', - * 'fields' => - * ), - * ... - * ), + * 'view'|'edit'|'create' => array( + * 'fields_type' => 'custom_list'|'twig'|'zlist', + * 'fields' => + * ), + * ... + * ), * ... * * @param \Silex\Application $oApp * @param ModuleDesign $oDesign + * * @return array * @throws Exception * @throws DOMFormatException @@ -950,32 +957,32 @@ class ApplicationHelper // Parsing form object class if ($oFormNode->GetUniqueElement('class')->GetText() !== null) { - // Parsing class + // Parsing class $sFormClass = $oFormNode->GetUniqueElement('class')->GetText(); - // Parsing properties - $aFormProperties = array( - 'display_mode' => static::FORM_DEFAULT_DISPLAY_MODE, - 'always_show_submit' => static::FORM_DEFAULT_ALWAYS_SHOW_SUBMIT, - ); - if($oFormNode->GetOptionalElement('properties') !== null) - { - foreach($oFormNode->GetOptionalElement('properties')->childNodes as $oPropertyNode) - { - switch($oPropertyNode->nodeName) - { - case 'display_mode': - $aFormProperties['display_mode'] = $oPropertyNode->GetText(static::FORM_DEFAULT_DISPLAY_MODE); - break; - case 'always_show_submit': - $aFormProperties['always_show_submit'] = ($oPropertyNode->GetText('false') === 'true') ? true : false; - break; - } - } - } + // Parsing properties + $aFormProperties = array( + 'display_mode' => static::FORM_DEFAULT_DISPLAY_MODE, + 'always_show_submit' => static::FORM_DEFAULT_ALWAYS_SHOW_SUBMIT, + ); + if ($oFormNode->GetOptionalElement('properties') !== null) + { + foreach ($oFormNode->GetOptionalElement('properties')->childNodes as $oPropertyNode) + { + switch ($oPropertyNode->nodeName) + { + case 'display_mode': + $aFormProperties['display_mode'] = $oPropertyNode->GetText(static::FORM_DEFAULT_DISPLAY_MODE); + break; + case 'always_show_submit': + $aFormProperties['always_show_submit'] = ($oPropertyNode->GetText('false') === 'true') ? true : false; + break; + } + } + } // Parsing availables modes for that form (view, edit, create, apply_stimulus) - $aFormStimuli = array(); + $aFormStimuli = array(); if (($oFormNode->GetOptionalElement('modes') !== null) && ($oFormNode->GetOptionalElement('modes')->GetNodes('mode')->length > 0)) { $aModes = array(); @@ -987,36 +994,37 @@ class ApplicationHelper } else { - throw new DOMFormatException('Mode tag must have an id attribute', null, null, $oFormNode); + throw new DOMFormatException('Mode tag must have an id attribute', null, null, + $oFormNode); } // If apply_stimulus mode, checking if stimuli are defined - if ($oModeNode->getAttribute('id') === 'apply_stimulus') - { - $oStimuliNode = $oModeNode->GetOptionalElement('stimuli'); - // if stimuli are defined, we overwrite the form that could have been set by the generic form - if($oStimuliNode !== null) - { - foreach ($oStimuliNode->GetNodes('stimulus') as $oStimulusNode) - { - $sStimulusCode = $oStimulusNode->getAttribute('id'); + if ($oModeNode->getAttribute('id') === 'apply_stimulus') + { + $oStimuliNode = $oModeNode->GetOptionalElement('stimuli'); + // if stimuli are defined, we overwrite the form that could have been set by the generic form + if ($oStimuliNode !== null) + { + foreach ($oStimuliNode->GetNodes('stimulus') as $oStimulusNode) + { + $sStimulusCode = $oStimulusNode->getAttribute('id'); - // Removing default form is present (in case the default forms were parsed before the current one (from current or parent class)) - if(isset($aForms[$sFormClass]['apply_stimulus'][$sStimulusCode])) - { - unset($aForms[$sFormClass]['apply_stimulus'][$sStimulusCode]); - } + // Removing default form is present (in case the default forms were parsed before the current one (from current or parent class)) + if (isset($aForms[$sFormClass]['apply_stimulus'][$sStimulusCode])) + { + unset($aForms[$sFormClass]['apply_stimulus'][$sStimulusCode]); + } - $aFormStimuli[] = $oStimulusNode->getAttribute('id'); - } - } - } + $aFormStimuli[] = $oStimulusNode->getAttribute('id'); + } + } + } } } else { - // If no mode was specified, we set it all but stimuli as it would have no sense that every transition forms - // have as many fields displayed as a regular edit form for example. + // If no mode was specified, we set it all but stimuli as it would have no sense that every transition forms + // have as many fields displayed as a regular edit form for example. $aModes = array('view', 'edit', 'create'); } @@ -1024,7 +1032,7 @@ class ApplicationHelper $aFields = array( 'id' => $oFormNode->getAttribute('id'), 'type' => null, - 'properties' => $aFormProperties, + 'properties' => $aFormProperties, 'fields' => null, 'layout' => null ); @@ -1058,7 +1066,8 @@ class ApplicationHelper } else { - throw new DOMFormatException('Field tag must have an id attribute', null, null, $oFormNode); + throw new DOMFormatException('Field tag must have an id attribute', null, null, + $oFormNode); } } } @@ -1086,60 +1095,62 @@ class ApplicationHelper // Adding form for each class / mode foreach ($aModes as $sMode) { - // Initializing current class if necessary + // Initializing current class if necessary if (!isset($aForms[$sFormClass])) { $aForms[$sFormClass] = array(); } if ($sMode === 'apply_stimulus') - { - // Iterating over current class and child classes to fill stimuli forms - foreach (MetaModel::EnumChildClasses($sFormClass, ENUM_CHILD_CLASSES_ALL) as $sChildClass) - { - // Initializing child class if necessary - if(!isset($aForms[$sChildClass][$sMode])) - { - $aForms[$sChildClass][$sMode] = array(); - } + { + // Iterating over current class and child classes to fill stimuli forms + foreach (MetaModel::EnumChildClasses($sFormClass, ENUM_CHILD_CLASSES_ALL) as $sChildClass) + { + // Initializing child class if necessary + if (!isset($aForms[$sChildClass][$sMode])) + { + $aForms[$sChildClass][$sMode] = array(); + } - // If stimuli are implicitly defined (empty tag), we define all those that have not already been by other forms. - $aChildStimuli = $aFormStimuli; - if(empty($aChildStimuli)) - { - // Stimuli already declared - $aDeclaredStimuli = array(); - if(array_key_exists($sChildClass, $aForms) && array_key_exists('apply_stimulus', $aForms[$sChildClass])) - { - $aDeclaredStimuli = array_keys($aForms[$sChildClass]['apply_stimulus']); - } - // All stimuli - $aDatamodelStimuli = array_keys(MetaModel::EnumStimuli($sChildClass)); - // Missing stimuli - $aChildStimuli = array_diff($aDatamodelStimuli, $aDeclaredStimuli); - } + // If stimuli are implicitly defined (empty tag), we define all those that have not already been by other forms. + $aChildStimuli = $aFormStimuli; + if (empty($aChildStimuli)) + { + // Stimuli already declared + $aDeclaredStimuli = array(); + if (array_key_exists($sChildClass, $aForms) && array_key_exists('apply_stimulus', + $aForms[$sChildClass])) + { + $aDeclaredStimuli = array_keys($aForms[$sChildClass]['apply_stimulus']); + } + // All stimuli + $aDatamodelStimuli = array_keys(MetaModel::EnumStimuli($sChildClass)); + // Missing stimuli + $aChildStimuli = array_diff($aDatamodelStimuli, $aDeclaredStimuli); + } - foreach($aChildStimuli as $sFormStimulus) - { - // Setting form if not defined OR if it was defined by a parent (abstract) class - if(!isset($aForms[$sChildClass][$sMode][$sFormStimulus]) || !empty($aFormStimuli)) - { - $aForms[$sChildClass][$sMode][$sFormStimulus] = $aFields; - $aForms[$sChildClass][$sMode][$sFormStimulus]['id'] = 'apply_stimulus-'.$sChildClass.'-'.$sFormStimulus; - } - } - } - } + foreach ($aChildStimuli as $sFormStimulus) + { + // Setting form if not defined OR if it was defined by a parent (abstract) class + if (!isset($aForms[$sChildClass][$sMode][$sFormStimulus]) || !empty($aFormStimuli)) + { + $aForms[$sChildClass][$sMode][$sFormStimulus] = $aFields; + $aForms[$sChildClass][$sMode][$sFormStimulus]['id'] = 'apply_stimulus-'.$sChildClass.'-'.$sFormStimulus; + } + } + } + } elseif (!isset($aForms[$sFormClass][$sMode])) { $aForms[$sFormClass][$sMode] = $aFields; } else { - throw new DOMFormatException('There is already a form for the class "' . $sFormClass . '" in "' . $sMode . '"', null, null, $oFormNode); + throw new DOMFormatException('There is already a form for the class "'.$sFormClass.'" in "'.$sMode.'"', + null, null, $oFormNode); } } - } + } else { throw new DOMFormatException('Class tag must be defined', null, null, $oFormNode); @@ -1147,40 +1158,40 @@ class ApplicationHelper } catch (DOMFormatException $e) { - throw new Exception('Could not create from [id="' . $oFormNode->getAttribute('id') . '"] from XML because of a DOM problem : ' . $e->getMessage()); + throw new Exception('Could not create from [id="'.$oFormNode->getAttribute('id').'"] from XML because of a DOM problem : '.$e->getMessage()); } catch (Exception $e) { - throw new Exception('Could not create from from XML : ' . $oFormNode->Dump() . ' ' . $e->getMessage()); + throw new Exception('Could not create from from XML : '.$oFormNode->Dump().' '.$e->getMessage()); } } return $aForms; } - /** - * Loads the scopes configuration from the module design XML - * - * @param \Silex\Application $oApp - * @param ModuleDesign $oDesign - */ - public static function LoadScopesConfiguration(Application $oApp, ModuleDesign $oDesign) - { - $oApp['scope_validator']->Init($oDesign->GetNodes('/module_design/classes/class')); - } + /** + * Loads the scopes configuration from the module design XML + * + * @param \Silex\Application $oApp + * @param ModuleDesign $oDesign + */ + public static function LoadScopesConfiguration(Application $oApp, ModuleDesign $oDesign) + { + $oApp['scope_validator']->Init($oDesign->GetNodes('/module_design/classes/class')); + } - /** - * Loads the lifecycle configuration from the module design XML - * - * @param \Silex\Application $oApp - * @param ModuleDesign $oDesign - */ - protected static function LoadLifecycleConfiguration(Application $oApp, ModuleDesign $oDesign) - { - $oApp['lifecycle_validator']->Init($oDesign->GetNodes('/module_design/classes/class')); - } + /** + * Loads the lifecycle configuration from the module design XML + * + * @param \Silex\Application $oApp + * @param ModuleDesign $oDesign + */ + protected static function LoadLifecycleConfiguration(Application $oApp, ModuleDesign $oDesign) + { + $oApp['lifecycle_validator']->Init($oDesign->GetNodes('/module_design/classes/class')); + } - /** + /** * Loads the context helper from the module design XML * * @param \Silex\Application $oApp @@ -1192,11 +1203,14 @@ class ApplicationHelper } /** - * Loads the classes lists from the module design XML. They are mainly used when searching an external key but could be used more extensively later + * Loads the classes lists from the module design XML. They are mainly used when searching an external key but + * could be used more extensively later * * @param \Silex\Application $oApp * @param ModuleDesign $oDesign + * * @return array + * @throws \DOMFormatException */ protected static function LoadListsConfiguration(Application $oApp, ModuleDesign $oDesign) { @@ -1221,7 +1235,8 @@ class ApplicationHelper $sListId = $oListNode->getAttribute('id'); if ($sListId === null) { - throw new DOMFormatException('List tag of "' . $sClassId . '" class must have an id attribute', null, null, $oListNode); + throw new DOMFormatException('List tag of "'.$sClassId.'" class must have an id attribute', null, + null, $oListNode); } // - Each items @@ -1230,7 +1245,8 @@ class ApplicationHelper $sItemId = $oItemNode->getAttribute('id'); if ($sItemId === null) { - throw new DOMFormatException('Item tag of "' . $sItemId . '" list must have an id attribute', null, null, $oItemNode); + throw new DOMFormatException('Item tag of "'.$sItemId.'" list must have an id attribute', null, + null, $oItemNode); } $aItem = array( @@ -1247,8 +1263,7 @@ class ApplicationHelper $aListItems[] = $aItem; } // - Sorting list items by rank - usort($aListItems, function($a, $b) - { + usort($aListItems, function ($a, $b) { return $a['rank'] > $b['rank']; }); $aClassLists[$sListId] = $aListItems; @@ -1261,86 +1276,74 @@ class ApplicationHelper } } - // Creating lists for child classes - // Note : This has been removed has we now dynamically look for the closest parent list only when necessary instead of generating list for child classes everytime - /* $aParentClasses = array_keys($aClassesLists); - foreach ($aParentClasses as $sParentClass) - { - foreach (MetaModel::EnumChildClasses($sParentClass) as $sChildClass) - { - // If the child class is not in the scope, we are going to try to add it - if (!in_array($sChildClass, $aParentClasses)) - { - $aClassesLists[$sChildClass] = $aClassesLists[$sParentClass]; - } - } - } */ - return $aClassesLists; } - /** - * Loads portal UI extensions - * - * @param \Silex\Application $oApp - * @return array - */ + /** + * Loads portal UI extensions + * + * @param \Silex\Application $oApp + * + * @return array + */ protected static function LoadUIExtensions(Application $oApp) - { - $aUIExtensions = array( - 'css_files' => array(), - 'css_inline' => null, - 'js_files' => array(), - 'js_inline' => null, - 'html' => array(), - ); - $aUIExtensionHooks = array( - iPortalUIExtension::ENUM_PORTAL_EXT_UI_BODY, - iPortalUIExtension::ENUM_PORTAL_EXT_UI_NAVIGATION_MENU, - iPortalUIExtension::ENUM_PORTAL_EXT_UI_MAIN_CONTENT, - ); + { + $aUIExtensions = array( + 'css_files' => array(), + 'css_inline' => null, + 'js_files' => array(), + 'js_inline' => null, + 'html' => array(), + ); + $aUIExtensionHooks = array( + iPortalUIExtension::ENUM_PORTAL_EXT_UI_BODY, + iPortalUIExtension::ENUM_PORTAL_EXT_UI_NAVIGATION_MENU, + iPortalUIExtension::ENUM_PORTAL_EXT_UI_MAIN_CONTENT, + ); - /** @var iPortalUIExtension $oExtensionInstance */ - foreach(MetaModel::EnumPlugins('iPortalUIExtension') as $oExtensionInstance) - { - // Adding CSS files - $aUIExtensions['css_files'] = array_merge($aUIExtensions['css_files'], $oExtensionInstance->GetCSSFiles($oApp)); + /** @var iPortalUIExtension $oExtensionInstance */ + foreach (MetaModel::EnumPlugins('iPortalUIExtension') as $oExtensionInstance) + { + // Adding CSS files + $aUIExtensions['css_files'] = array_merge($aUIExtensions['css_files'], + $oExtensionInstance->GetCSSFiles($oApp)); - // Adding CSS inline - $sCSSInline = $oExtensionInstance->GetCSSInline($oApp); - if($sCSSInline !== null) - { - $aUIExtensions['css_inline'] .= "\n\n" . $sCSSInline; - } + // Adding CSS inline + $sCSSInline = $oExtensionInstance->GetCSSInline($oApp); + if ($sCSSInline !== null) + { + $aUIExtensions['css_inline'] .= "\n\n".$sCSSInline; + } - // Adding JS files - $aUIExtensions['js_files'] = array_merge($aUIExtensions['js_files'], $oExtensionInstance->GetJSFiles($oApp)); + // Adding JS files + $aUIExtensions['js_files'] = array_merge($aUIExtensions['js_files'], + $oExtensionInstance->GetJSFiles($oApp)); - // Adding JS inline - $sJSInline = $oExtensionInstance->GetJSInline($oApp); - if($sJSInline !== null) - { - // Note: Semi-colon is to prevent previous script that would have omitted it. - $aUIExtensions['js_inline'] .= "\n\n;\n" . $sJSInline; - } + // Adding JS inline + $sJSInline = $oExtensionInstance->GetJSInline($oApp); + if ($sJSInline !== null) + { + // Note: Semi-colon is to prevent previous script that would have omitted it. + $aUIExtensions['js_inline'] .= "\n\n;\n".$sJSInline; + } - // Adding HTML for each hook - foreach($aUIExtensionHooks as $sUIExtensionHook) - { - $sFunctionName = 'Get'.$sUIExtensionHook.'HTML'; - $sHTML = $oExtensionInstance->$sFunctionName($oApp); - if($sHTML !== null) - { - if(!array_key_exists($sUIExtensionHook, $aUIExtensions['html'])) - { - $aUIExtensions['html'][$sUIExtensionHook] = ''; - } - $aUIExtensions['html'][$sUIExtensionHook] .= "\n\n" . $sHTML; - } - } - } + // Adding HTML for each hook + foreach ($aUIExtensionHooks as $sUIExtensionHook) + { + $sFunctionName = 'Get'.$sUIExtensionHook.'HTML'; + $sHTML = $oExtensionInstance->$sFunctionName($oApp); + if ($sHTML !== null) + { + if (!array_key_exists($sUIExtensionHook, $aUIExtensions['html'])) + { + $aUIExtensions['html'][$sUIExtensionHook] = ''; + } + $aUIExtensions['html'][$sUIExtensionHook] .= "\n\n".$sHTML; + } + } + } - return $aUIExtensions; - } + return $aUIExtensions; + } } diff --git a/datamodels/2.x/itop-portal-base/portal/src/routers/managebrickrouter.class.inc.php b/datamodels/2.x/itop-portal-base/portal/src/routers/managebrickrouter.class.inc.php index 07e20811e..3d9d84d91 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/routers/managebrickrouter.class.inc.php +++ b/datamodels/2.x/itop-portal-base/portal/src/routers/managebrickrouter.class.inc.php @@ -1,6 +1,6 @@ '/manage/{sBrickId}/{sGroupingTab}', + array( + 'pattern' => '/manage/{sBrickId}/{sDisplayType}/{sGroupingTab}', 'callback' => 'Combodo\\iTop\\Portal\\Controller\\ManageBrickController::DisplayAction', 'bind' => 'p_manage_brick', - 'values' => array('sGroupingTab' => null) + 'asserts' => array( + 'sDisplayType' => 'badge|pie-chart|bar-chart|top-list|default' + ), + 'values' => array( + 'sDisplayType' => null, // will be set using brick's XML config + 'sGroupingTab' => null + ) ), - array('pattern' => '/manage/{sBrickId}/{sGroupingTab}/{sGroupingArea}/page/{iPageNumber}/show/{iListLength}', + array( + 'pattern' => '/manage/{sBrickId}/{sGroupingTab}/{sGroupingArea}/page/{iPageNumber}/show/{iListLength}', 'callback' => 'Combodo\\iTop\\Portal\\Controller\\ManageBrickController::DisplayAction', 'bind' => 'p_manage_brick_lazy', 'asserts' => array( @@ -41,7 +47,14 @@ class ManageBrickRouter extends AbstractRouter 'iPageNumber' => '1', 'iListLength' => '20' ) - ) + ), + array( + 'pattern' => '/manage/export/excel/start/{sBrickId}/{sGroupingTab}/{sGroupingArea}', + 'callback' => 'Combodo\\iTop\\Portal\\Controller\\ManageBrickController::ExcelExportStartAction', + 'bind' => 'p_manage_brick_excel_export_start', + 'asserts' => array(), + 'values' => array(), + ), ); } diff --git a/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/layout-chart.html.twig b/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/layout-chart.html.twig new file mode 100644 index 000000000..c9ffa1db3 --- /dev/null +++ b/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/layout-chart.html.twig @@ -0,0 +1,15 @@ +{# itop-portal-base/portal/src/views/bricks/manage/layout-chart.html.twig #} +{# Manage brick base layout #} +{% extends 'itop-portal-base/portal/src/views/bricks/manage/layout.html.twig' %} + +{% block pPageBodyClass %}{{ parent() }} page_manage_brick{% endblock %} + + +{% block pMainContentHolder %} +
+
+ {% include 'itop-portal-base/portal/src/views/bricks/manage/mode-' ~ sDisplayType ~ '.html.twig' with {'oBrick': oBrick, 'aColumns': aColumns, 'aNames': aNames, 'aUrls': aUrls, 'aDisplayValues': aDisplayValues} %} +
+
+{% endblock %} + diff --git a/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/layout-table.html.twig b/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/layout-table.html.twig new file mode 100644 index 000000000..e27e49009 --- /dev/null +++ b/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/layout-table.html.twig @@ -0,0 +1,339 @@ +{# itop-portal-base/portal/src/views/bricks/manage/layout-table.html.twig #} +{# Manage brick base layout #} +{% extends 'itop-portal-base/portal/src/views/bricks/manage/layout.html.twig' %} + +{% block pPageBodyClass %}{{ parent() }} page_manage_brick{% endblock %} + +{% block pMainContentHolder %} + {% if aGroupingTabsValues|length > 1 %} + + {% endif %} + {% set iTableCount = 0 %} + {% if aGroupingAreasData|length > 0 %} + {% for aAreaData in aGroupingAreasData %} + {% if aAreaData.iItemsCount > 0 %} + {% set iTableCount = iTableCount + 1 %} +
+
+

{{ aAreaData.sTitle }}

+ {% if bCanExport %} + + + + {% endif %} +
+
+
+
+
+ {% endif %} + {% endfor %} + {% endif %} + + {% if iTableCount == 0 %} +
+
+

{{ 'Brick:Portal:Manage:Table:NoData'|dict_s }}

+
+
+ {% endif %} +{% endblock %} + +{% block pPageLiveScripts %} + {{ parent() }} + + +{% endblock %} \ No newline at end of file diff --git a/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/layout.html.twig b/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/layout.html.twig index 0cde6ee35..d58b6c36e 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/layout.html.twig +++ b/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/layout.html.twig @@ -1,361 +1,17 @@ {# itop-portal-base/portal/src/views/bricks/manage/layout.html.twig #} -{# Manage brick base layout #} {% extends 'itop-portal-base/portal/src/views/bricks/layout.html.twig' %} -{% block pPageBodyClass %}{{ parent() }} page_manage_brick{% endblock %} - -{% block pMainHeaderTitle %} - {{ oBrick.GetTitle()|dict_s }} -{% endblock %} +{% block pMainHeaderTitle %}{{ oBrick.GetTitle()|dict_s }} ({{ iCount }}) {% endblock %} {% block pMainHeaderActions %} - {% if aGroupingTabsValues|length > 1 %} - - {% endif %} +
+ {% for sDisplay in oBrick.ListLayoutDisplayTypes %} + + {{ ('Brick:Portal:Manage:DisplayType:' ~ sDisplay)|dict_s }} + + {% endfor %} +
{% endblock %} -{% block pMainContentHolder%} - {% set iTableCount = 0 %} - {% if aGroupingAreasData|length > 0 %} - {% for aAreaData in aGroupingAreasData %} - {% if aAreaData.iItemsCount > 0 %} - {% set iTableCount = iTableCount + 1 %} -
-
-

{{ aAreaData.sTitle }}

-
-
- {# We decided not to show empty tables anymore #} - {# - {% if aAreaData.iItemsCount > 0 %} - #} -
- {# - {% else %} -
- {{ 'Brick:Portal:Manage:Table:NoData'|dict_s }} -
- {% endif %} - #} -
-
- {% endif %} - {% endfor %} - {% endif %} - - {% if iTableCount == 0 %} -
-
-

{{ 'Brick:Portal:Manage:Table:NoData'|dict_s }}

-
-
- {% endif %} -{% endblock %} - -{% block pPageLiveScripts %} - {{ parent() }} - - -{% endblock %} \ No newline at end of file diff --git a/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/mode-bar-chart.html.twig b/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/mode-bar-chart.html.twig new file mode 100644 index 000000000..5d833af95 --- /dev/null +++ b/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/mode-bar-chart.html.twig @@ -0,0 +1,62 @@ +{% if aNames|length > 0 %} +
+ +{% else %} +

{{ 'Brick:Portal:Manage:Table:NoData'|dict_s }}

+{% endif %} \ No newline at end of file diff --git a/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/mode-pie-chart.html.twig b/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/mode-pie-chart.html.twig new file mode 100644 index 000000000..bf266b3da --- /dev/null +++ b/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/mode-pie-chart.html.twig @@ -0,0 +1,36 @@ +{% if aNames|length > 0 %} +
+ +{% else %} +

{{ 'Brick:Portal:Manage:Table:NoData'|dict_s }}

+{% endif %} \ No newline at end of file diff --git a/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/popup-export-excel.html.twig b/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/popup-export-excel.html.twig new file mode 100644 index 000000000..a6922d0ff --- /dev/null +++ b/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/popup-export-excel.html.twig @@ -0,0 +1,57 @@ +{# itop-portal-base/popup-export-excel.html.twig #} +{# Export Excel popup layout #} + + + + + + diff --git a/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/tile-badge.html.twig b/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/tile-badge.html.twig new file mode 100644 index 000000000..86315404f --- /dev/null +++ b/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/tile-badge.html.twig @@ -0,0 +1,28 @@ +{# itop-portal-dashlet-basic/tile.html.twig #} +{# Image brick tile layout #} + + \ No newline at end of file diff --git a/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/tile-chart.html.twig b/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/tile-chart.html.twig new file mode 100644 index 000000000..498cf6b5c --- /dev/null +++ b/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/tile-chart.html.twig @@ -0,0 +1,20 @@ +{# itop-portal-dashlet-basic/tile.html.twig #} +{# Image brick tile layout #} + + \ No newline at end of file diff --git a/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/tile-default.html.twig b/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/tile-default.html.twig new file mode 100644 index 000000000..91bfb4a24 --- /dev/null +++ b/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/tile-default.html.twig @@ -0,0 +1,21 @@ +{# itop-portal-base/portal/src/views/bricks/tile.html.twig #} +{# Base brick tile layout #} + + \ No newline at end of file diff --git a/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/tile-top-list.html.twig b/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/tile-top-list.html.twig new file mode 100644 index 000000000..57416ef55 --- /dev/null +++ b/datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/tile-top-list.html.twig @@ -0,0 +1,40 @@ +{# itop-portal-dashlet-basic/tile.html.twig #} +{# Topx List tile layout #} + + \ No newline at end of file diff --git a/datamodels/2.x/itop-portal-base/portal/web/css/portal.css b/datamodels/2.x/itop-portal-base/portal/web/css/portal.css index f0c9556bd..e653c4bc0 100644 --- a/datamodels/2.x/itop-portal-base/portal/web/css/portal.css +++ b/datamodels/2.x/itop-portal-base/portal/web/css/portal.css @@ -6,53 +6,53 @@ /*******************/ @media (max-width: 768px) { body { - padding-top: 60px; + padding-top: 60px; } body.home { - padding-top: 70px; + padding-top: 70px; } } footer { margin: 5em 1em; - text-align: center; + text-align: center; } /* Environment banner */ #envbanner { position: relative; z-index: 10; padding: 5px 15px; - text-align: center; + text-align: center; } #envbanner > button { margin-left: 5px; - color: #000; + color: #000; } /* Navigation menu */ .navbar-nav .dropdown-menu a .glyphicon, .user_infos .dropdown-menu a .glyphicon { - margin-right: 15px; + margin-right: 15px; } .nav > li > a > span.brick_icon, .dropdown-menu > li > a > span.brick_icon { margin-right: 20px; - vertical-align: sub; + vertical-align: sub; } /* Topbar */ #topbar .navbar-header { position: relative; - z-index: 2; + z-index: 2; } #topbar .navbar-collapse { position: relative; z-index: 1; - overflow-y: auto; + overflow-y: auto; } #topbar .navbar-collapse > .navbar-nav { - padding-top: 30px; + padding-top: 30px; } #topbar .navbar-brand > img { - max-height: 100%; + max-height: 100%; } #topbar .user_infos { - text-decoration: none; + text-decoration: none; } #topbar .user_photo { position: absolute; @@ -66,7 +66,7 @@ footer { background-color: #585653; border: 2px solid #fff; border-radius: 100%; - box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4); + box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4); } #topbar .user_fullname { display: inline-block; @@ -76,7 +76,7 @@ footer { /*color: $white;*/ white-space: nowrap; text-overflow: ellipsis; - overflow-x: hidden; + overflow-x: hidden; } /* Sidebar */ @media (min-width: 768px) { @@ -86,11 +86,11 @@ footer { left: 0px; padding: 0px; /* Overriding BS */ - height: 100%; + height: 100%; } #sidebar .user_card { padding: 30px 0px; - text-align: center; + text-align: center; } #sidebar .user_card .user_photo { margin: 0px auto 10px auto; @@ -101,61 +101,61 @@ footer { background-color: #585653; background-repeat: no-repeat; border: 2px solid #fff; - border-radius: 100%; + border-radius: 100%; } #sidebar .user_card .user_infos { font-size: 1em; - color: #fff; + color: #fff; } #sidebar .user_card .user_infos .dropdown-toggle { - color: #fff; + color: #fff; } #sidebar .user_card .user_options.dropdown-menu { width: 92%; - left: 4%; + left: 4%; } #sidebar .user_card .user_fullname { - font-weight: 600; + font-weight: 600; } #sidebar .menu { max-height: 59%; overflow-y: auto; - overflow-x: hidden; + overflow-x: hidden; } #sidebar .menu .navbar-nav > li { - width: 100%; + width: 100%; } #sidebar .menu .navbar-nav > li > a > .brick_icon { width: 1.2em; vertical-align: sub; text-align: center; - margin-right: 10px; + margin-right: 10px; } #sidebar .logo { position: absolute; bottom: 15px; width: 100%; - text-align: center; + text-align: center; } #sidebar .logo img { width: 40%; - max-width: 100%; + max-width: 100%; } } /* Warning : Not a offical BS breakpoint */ @media (min-width: 1600px) { #sidebar .user_card .user_photo { width: 120px; - height: 120px; + height: 120px; } #sidebar .menu .nav > li > a > .brick_icon { - margin-right: 20px; + margin-right: 20px; } } /* Main content */ @media (min-width: 768px) { #main-wrapper { - margin-top: 20px; + margin-top: 20px; } } /* Overlays*/ @@ -168,18 +168,18 @@ footer { width: 100%; height: 100%; background-color: black; - opacity: 0.5; + opacity: 0.5; } #page_overlay .overlay_content { margin-top: 20em; width: 100%; - color: white; + color: white; } .overlay_content { - text-align: center; + text-align: center; } .content_loader { - text-align: center; + text-align: center; } .content_loader .icon { margin-bottom: 0.3em; @@ -192,15 +192,15 @@ footer { animation: spin 1.2s linear infinite; -webkit-animation: spin 1.2s linear infinite; -moz-animation: spin 1.2s linear infinite; - -ms-animation: spin 1.2s linear infinite; + -ms-animation: spin 1.2s linear infinite; } .content_loader .message { font-size: 1.5em; - /* 2em; */ + /* 2em; */ } .datatables_overlay { padding: 5% 0px !important; - background-color: white; + background-color: white; } /******************/ /* Global classes */ @@ -225,7 +225,7 @@ footer { -moz-box-pack: center; -ms-flex-pack: center; -webkit-justify-content: center; - justify-content: center; + justify-content: center; } /*********************/ /* Global animations */ @@ -233,22 +233,22 @@ footer { /* Spin */ @keyframes spin { 100% { - transform: rotate(360deg); + transform: rotate(360deg); } } @-webkit-keyframes spin { 100% { - -webkit-transform: rotate(360deg); + -webkit-transform: rotate(360deg); } } @-moz-keyframes spin { 100% { - -moz-transform: rotate(360deg); + -moz-transform: rotate(360deg); } } @-ms-keyframes spin { 100% { - -ms-transform: rotate(360deg); + -ms-transform: rotate(360deg); } } /*********************/ @@ -256,61 +256,61 @@ footer { /*********************/ .list-group.tree { margin-top: 11px; - margin-bottom: -11px; + margin-bottom: -11px; } .list-group.tree .list-group-item { padding-right: 0px; - /* To align all actions on the right without indent */ + /* To align all actions on the right without indent */ } /******************/ /* Modal settings */ /******************/ .modal-content .content_loader { margin: 7em 0em; - text-align: center; + text-align: center; } /**************************/ /* MagnificPopup settings */ /**************************/ .mfp-bg { - z-index: 1200; + z-index: 1200; } .mfp-wrap { - z-index: 1210; + z-index: 1210; } .mfp-img { cursor: pointer; - cursor: zoom-out; + cursor: zoom-out; } /********************/ /* Typeahed setting */ /********************/ .twitter-typeahead .tt-menu { max-height: 200px; - overflow-y: auto; + overflow-y: auto; } @media (min-width: 768px) { .twitter-typeahead .tt-menu { - max-height: 300px; + max-height: 300px; } } .twitter-typeahead .tt-dataset > .content_loader { margin: 10px 0px; text-align: center; - font-size: 0.6em; + font-size: 0.6em; } .twitter-typeahead .tt-dataset > .content_loader .icon { - height: 25px; + height: 25px; } .twitter-typeahead .tt-dataset .no_result { text-align: center; - font-style: italic; + font-style: italic; } /*****************/ /* Home settings */ /*****************/ .home #main-wrapper { - padding-top: 15px; + padding-top: 15px; } .home .tile { display: block; @@ -325,20 +325,61 @@ footer { text-decoration: none; white-space: normal; line-height: 4em; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); } .home .tile .tile_decoration { position: absolute; top: 0.3em; - left: 2.5em; + left: 2.5em; } .home .tile .tile_title { font-weight: bold; - color: #333; + color: #333; +} +.home .tile .tile_title > span.icon { + color: #ea7d1e; } .home .tile .tile_description { display: none; - color: #555; + color: #555; +} +.home div.tile-badge > a.tile { + display: table; + width: 100%; +} +.home div.tile-badge > a.tile > div { + display: table-row; +} +.home div.tile-badge > a.tile > div > div { + display: table-cell; +} +.home div.tile-badge > a.tile > div > div.tile_decoration { + text-align: center; + vertical-align: top; + position: inherit; + float: inherit; +} +.home div.tile-badge > a.tile > div > div.tile_body { + text-align: right; + padding-left: 0; +} +.home div.tile-badge > a.tile > div > div.tile_body > div:first-child { + text-align: center; + margin: 0 auto; + margin-left: 10%; +} +.home div.tile-badge > a.tile > div > div.tile_body > div:first-child > div { + text-align: center; +} +.home div.tile-badge > a.tile > div > div.tile_body > div:first-child > div.tile_description { + margin-bottom: 1.5em; +} +.home div.tile-badge > a.tile > div > div.tile_body > div:first-child > div.tile_title { + font-size: 1.5em; + font-weight: bold; +} +.home div.tile-badge > a.tile > div > div.tile_body > div.tile_description { + text-align: right; } @media (min-width: 768px) { .home .tile { @@ -347,10 +388,10 @@ footer { padding: 40px 40px 30px 40px; min-height: 10em; text-align: left; - transition: all 0.2s linear; + transition: all 0.2s linear; } .home .tile:hover { - box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.10); + box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.10); } .home .tile .tile_decoration { display: block; @@ -358,75 +399,75 @@ footer { float: left; top: 1.5em; left: initial; - margin: 0px 30px 15px 0px; + margin: 0px 30px 15px 0px; } .home .tile .tile_body { display: block; padding-left: 4.3em; text-align: left; - line-height: 1.5em; + line-height: 1.5em; } .home .tile .tile_title { margin-bottom: 1em; - font-size: 1em; + font-size: 1em; } .home .tile .tile_description { display: block; - text-align: left; + text-align: left; } } @media (min-width: 992px) { .home .tile { min-height: 14em; - padding: 30px 40px 30px 40px; + padding: 30px 40px 30px 40px; } .home .tile .tile_decoration > span.icon { - font-size: 4em; + font-size: 4em; } .home .tile .tile_body { - padding-left: 6.3em; + padding-left: 6.3em; } .home .tile .tile_title { - font-size: 1.4em; + font-size: 1.4em; } .home .tile .tile_description { - font-size: 1.2em; + font-size: 1.2em; } } @media (min-width: 1200px) { .home .tile { margin-bottom: 40px; min-height: 15em; - padding: 40px 50px 30px 50px; + padding: 40px 50px 30px 50px; } .home .tile .tile_decoration { margin: 0px 40px 15px 0px; - top: 1.5em; + top: 1.5em; } .home .tile .tile_decoration > span.icon { - font-size: 6em; + font-size: 6em; } .home .tile .tile_body { - padding-left: 9.1em; + padding-left: 9.1em; } .home .tile .tile_title { - font-size: 1.5em; + font-size: 1.5em; } .home .tile .tile_description { - font-size: 1.2em; + font-size: 1.2em; } } /********************/ /* Modules settings */ /********************/ #main-header { - text-align: center; + text-align: center; } #main-header-title { - margin-bottom: 15px; + margin-bottom: 15px; } #main-header-actions { - margin-bottom: 15px; + margin-bottom: 15px; } /* This is no longer necessary but we keep it just in case */ /*#main-header-actions .btn-group .btn{ @@ -437,39 +478,39 @@ footer { }*/ @media (min-width: 768px) { #main-header:after { - clear: both; + clear: both; } #main-header-title { float: left; margin-bottom: 0px; min-height: 6em; - text-align: left; + text-align: left; } #main-header-actions { float: right; - margin-bottom: 0px; + margin-bottom: 0px; } } .dataTables_wrapper { - padding: 10px 10px; + padding: 10px 10px; } .dataTable.table td img { max-width: 100%; - height: initial !important; + height: initial !important; } #brick_content_toolbar { /* margin: 10px 0px 6px 0px; */ - padding: 10px; + padding: 10px; } #brick_content_toolbar > div label { font-weight: normal; white-space: nowrap; - text-align: left; + text-align: left; } #brick_content_toolbar > div label input { margin-left: 0.5em; display: inline-block; - width: 130px; + width: 130px; } /***********************/ /* Brick communication */ @@ -479,12 +520,12 @@ footer { padding: 20px; background-color: #ededed; border: none; - font-weight: initial; + font-weight: initial; } .home .tile_communication .carousel { margin-bottom: 0px; width: 100%; - height: 200px; + height: 200px; } /**********************/ /* Brick user profile */ @@ -500,35 +541,35 @@ footer { text-align: center; color: white; background-color: black; - opacity: 0.5; + opacity: 0.5; } #user-profile-wrapper .user_profile_picture .preview { display: inline-block; position: relative; max-width: 175px; max-height: 175px; - overflow: hidden; + overflow: hidden; } #user-profile-wrapper .user_profile_picture .preview img { max-width: 100%; - max-height: 100%; + max-height: 100%; } #user-profile-wrapper .user_profile_picture .actions { display: inline-block; vertical-align: top; /*middle;*/ - margin-left: 5px; + margin-left: 5px; } #user-profile-wrapper .user_profile_picture .actions .btn { display: block; position: relative; - margin-bottom: 10px; + margin-bottom: 10px; } #user-profile-wrapper .user_profile_picture .actions .btn:last-child { - margin-bottom: 0px; + margin-bottom: 0px; } #user-profile-wrapper .user_profile_picture .actions .btn.btn_edit { - overflow: hidden; + overflow: hidden; } #user-profile-wrapper .user_profile_picture .actions .btn.btn_edit input { position: absolute; @@ -537,7 +578,7 @@ footer { width: 100%; height: 100%; opacity: 0; - cursor: pointer; + cursor: pointer; } /****************/ /* Brick browse */ @@ -546,51 +587,51 @@ footer { /****************/ #brick_content_tree { position: relative; - margin-top: 0px; + margin-top: 0px; } #brick_content_tree .list-group-item { - padding-top: 0px; + padding-top: 0px; } #brick_content_tree .list-group-item > .tree-item-wrapper { display: block; padding-top: 10px; color: inherit; text-decoration: inherit; - cursor: pointer; + cursor: pointer; } .list-group-item > .list-group-item-actions { /*display: none; Displaying actions only when hovering was not unanimous in the team */ position: absolute; top: 10px; - right: 10px; + right: 10px; } .list-group-item:hover > .list-group-item-actions, .mosaic-group-item:hover > .mosaic-group-item-actions { - display: block; + display: block; } .list-group-item .list-group-item-actions a:not(:first-child), .mosaic-group-item .mosaic-group-item-actions a:not(:first-child) { - margin-left: 10px; + margin-left: 10px; } .list-group-item .keep-spinning { animation: spin 1s linear infinite; -webkit-animation: spin 1s linear infinite; -moz-animation: spin 1s linear infinite; - -ms-animation: spin 1s linear infinite; + -ms-animation: spin 1s linear infinite; } .list-group.tree .list-group-item .list-group-item-description { display: block; margin-top: 3px; - font-size: 0.8em; + font-size: 0.8em; } /* Secondary actions */ .list-group-item-actions .group-actions-wrapper, .mosaic-group-item-actions .group-actions-wrapper, table .group-actions-wrapper { - text-align: center; + text-align: center; } table .group-actions { - position: relative; + position: relative; } .list-group-item-actions a.glyphicon-menu-hamburger, .mosaic-group-item-actions a.glyphicon-menu-hamburger, table .group-actions a.glyphicon-menu-hamburger { cursor: pointer; - text-decoration: none; + text-decoration: none; } .list-group-item-actions .item-action-wrapper, .mosaic-group-item-actions .item-action-wrapper, table .group-actions .item-action-wrapper { display: none; @@ -600,33 +641,33 @@ table .group-actions { right: 15px; -webkit-box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.15); -moz-box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.15); - box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.15); + box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.15); } .list-group-item-actions .item-action-wrapper .glyphicon, .mosaic-group-item-actions .item-action-wrapper .glyphicon, table .group-actions .item-action-wrapper .glyphicon { - margin-right: 0.6em; + margin-right: 0.6em; } .list-group-item-actions .item-action-wrapper.collapse.in, .mosaic-group-item-actions .item-action-wrapper.collapse.in, table .group-actions .item-action-wrapper.collapse.in { - display: block; + display: block; } .list-group-item-actions .item-action-wrapper .panel-body > p, .mosaic-group-item-actions .item-action-wrapper .panel-body > p, table .group-actions .item-action-wrapper .panel-body > p { text-align: left; - white-space: nowrap; + white-space: nowrap; } .list-group-item-actions .item-action-wrapper .panel-body > p:last-child, .mosaic-group-item-actions .item-action-wrapper .panel-body > p:last-child, table .group-actions .item-action-wrapper .panel-body > p:last-child { - margin-bottom: 0px; + margin-bottom: 0px; } #brick_content_empty { display: none; padding: 40px; font-size: 1.3em; - font-style: italic; + font-style: italic; } /* Loader */ #brick_tree_overlay, #brick_mosaic_overlay { display: none; padding: 8% 0px; border-radius: 0px 0px 4px 4px; - font-size: 1em; + font-size: 1em; } /****************************************************************/ /* - Mosaic mode */ @@ -643,7 +684,7 @@ table .group-actions { /****************************************************************/ #brick_content_mosaic { position: relative; - padding: 10px 10px 1px 10px; + padding: 10px 10px 1px 10px; } /* Breadcrumb */ #mosaic-breadcrumb { @@ -652,24 +693,24 @@ table .group-actions { font-size: 12px; white-space: nowrap; overflow-x: hidden; - text-overflow: ellipsis; + text-overflow: ellipsis; } .mosaic-group { - display: none; + display: none; } /* Only the first level is showed by default */ .mosaic-group:first-of-type { - display: block; + display: block; } .mosaic-group-back, .mosaic-group-item { position: relative; height: 55px; margin-bottom: 10px; text-align: center; - color: #fff; + color: #fff; } .mosaic-group-back { - font-size: 25px; + font-size: 25px; } .mosaic-item { display: table; @@ -677,75 +718,83 @@ table .group-actions { height: 100%; overflow: hidden; background-color: #585653; - transition: all linear 0.3s; + transition: all linear 0.3s; } .mosaic-item, .mosaic-item:hover, .mosaic-item:active, .mosaic-item:focus, .mosaic-item:visited { color: #fff; - text-decoration: none; + text-decoration: none; } .mosaic-item:active { - background-color: #9e510f; + background-color: #9e510f; } .mosaic-item-image, .mosaic-item-text { display: table-cell; text-align: center; - vertical-align: middle; + vertical-align: middle; } .mosaic-item-image > img { - max-width: 85%; + max-width: 85%; } .mosaic-item-text { max-width: 1px; /* This is an arbitrary value. It is just here to make .mosaic-item-name wrap when there is a very long word in it. */ - overflow: hidden; + overflow: hidden; } .mosaic-group-item > .mosaic-group-item-actions { position: absolute; top: 5px; - right: 5px; + right: 5px; } .mosaic-group-item-actions > a { color: #fff; - text-decoration: none; + text-decoration: none; } .mosaic-group-item-actions > a:hover, .mosaic-group-item-actions > a:focus { - color: #eee; + color: #eee; } @media (max-width: 768px) { /* All layouts */ /* Layout 2 */ /* Layout 5/7 */ - .mosaic-group-item > .mosaic-group-item-actions { - top: 12px; - right: 6px; - } - .mosaic-group-item > .mosaic-group-item-actions > .glyphicon { - margin-top: 5px; - } - .mosaic-group-item-actions > a { - font-size: 20px; - } - .mosaic-item-image { - width: 55px; - padding: 10px; - } - .mosaic-item-image > img { - max-height: 30px; - } - .mosaic-item-name { - font-size: 14px; - max-height: 50px; - overflow: hidden; - } - .mosaic-item-description { - display: none; - } - .mosaic-item-layout-2 .mosaic-item-description { - display: block; - } - .mosaic-item-layout-5 .mosaic-item-name, .mosaic-item-layout-7 .mosaic-item-name { - padding-right: 40px; - } + .mosaic-group-item > .mosaic-group-item-actions { + top: 12px; + right: 6px; + } + + .mosaic-group-item > .mosaic-group-item-actions > .glyphicon { + margin-top: 5px; + } + + .mosaic-group-item-actions > a { + font-size: 20px; + } + + .mosaic-item-image { + width: 55px; + padding: 10px; + } + + .mosaic-item-image > img { + max-height: 30px; + } + + .mosaic-item-name { + font-size: 14px; + max-height: 50px; + overflow: hidden; + } + + .mosaic-item-description { + display: none; + } + + .mosaic-item-layout-2 .mosaic-item-description { + display: block; + } + + .mosaic-item-layout-5 .mosaic-item-name, .mosaic-item-layout-7 .mosaic-item-name { + padding-right: 40px; + } } @media (min-width: 768px) { /* All layouts */ @@ -754,97 +803,110 @@ table .group-actions { }*/ /* Layout 1 */ /* Layout 7 */ - .mosaic-group-item { - display: inline-block; - width: 32%; - height: 120px; - margin-right: 1.95%; - /* We don't put 2% to keep a margin in case of a bad browser rendering */ - word-break: break-word; - } - .mosaic-item { - padding: 10px; - } - .mosaic-item:hover, .mosaic-item:focus { - background-color: #ea7d1e; - box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.20); - } - .mosaic-item-text .mosaic-item-text-wrapper { - max-height: 100px; - /* Must be .mosaic-item absolute height (in px) */ - } - .mosaic-item-name { - max-height: 100%; - /* It's ok if description is pushed down and not visible; but we truncate before it flows out of the tile */ - overflow: hidden; - font-weight: 600; - font-size: 12px; - } - .mosaic-item-description { - overflow: hidden; - } - .mosaic-item-layout-1 .mosaic-item-name { - font-weight: inherit; - font-size: 14px; - } - .mosaic-item-layout-7 .mosaic-item-image { - display: none; - } - .mosaic-item-layout-3 .mosaic-item-description, .mosaic-item-layout-7 .mosaic-item-description { - margin-top: 10px; - max-height: 40px; - font-size: 10px; - } + .mosaic-group-item { + display: inline-block; + width: 32%; + height: 120px; + margin-right: 1.95%; + /* We don't put 2% to keep a margin in case of a bad browser rendering */ + word-break: break-word; + } + + .mosaic-item { + padding: 10px; + } + + .mosaic-item:hover, .mosaic-item:focus { + background-color: #ea7d1e; + box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.20); + } + + .mosaic-item-text .mosaic-item-text-wrapper { + max-height: 100px; + /* Must be .mosaic-item absolute height (in px) */ + } + + .mosaic-item-name { + max-height: 100%; + /* It's ok if description is pushed down and not visible; but we truncate before it flows out of the tile */ + overflow: hidden; + font-weight: 600; + font-size: 12px; + } + + .mosaic-item-description { + overflow: hidden; + } + + .mosaic-item-layout-1 .mosaic-item-name { + font-weight: inherit; + font-size: 14px; + } + + .mosaic-item-layout-7 .mosaic-item-image { + display: none; + } + + .mosaic-item-layout-3 .mosaic-item-description, .mosaic-item-layout-7 .mosaic-item-description { + margin-top: 10px; + max-height: 40px; + font-size: 10px; + } } @media (min-width: 992px) { /* Layout 5 & 7 */ - .mosaic-item { - padding: 10px 15px; - } - .mosaic-group-back { - font-size: 40px; - } - .mosaic-item-layout-5 .mosaic-item-image, .mosaic-item-layout-7 .mosaic-item-image { - display: table-cell; - width: 105px; - padding-left: 5px; - padding-right: 18px; - } - .mosaic-item-layout-5 .mosaic-item-image > img, .mosaic-item-layout-7 .mosaic-item-image > img { - max-width: 105px; - /* Equals parent element width */ - } - .mosaic-item-layout-5 .mosaic-item-name, .mosaic-item-layout-7 .mosaic-item-name { - font-size: 12px; - } + .mosaic-item { + padding: 10px 15px; + } + + .mosaic-group-back { + font-size: 40px; + } + + .mosaic-item-layout-5 .mosaic-item-image, .mosaic-item-layout-7 .mosaic-item-image { + display: table-cell; + width: 105px; + padding-left: 5px; + padding-right: 18px; + } + + .mosaic-item-layout-5 .mosaic-item-image > img, .mosaic-item-layout-7 .mosaic-item-image > img { + max-width: 105px; + /* Equals parent element width */ + } + + .mosaic-item-layout-5 .mosaic-item-name, .mosaic-item-layout-7 .mosaic-item-name { + font-size: 12px; + } } @media (min-width: 1200px) { /* All layouts */ - .mosaic-group-item { - width: 24%; - height: 140px; - margin-right: 1.3%; - } - .mosaic-item-text .mosaic-item-text-wrapper { - max-height: 120px; - /* Must be .mosaic-item absolute height (in px) */ - /* overflow hidden inherited */ - } + .mosaic-group-item { + width: 24%; + height: 140px; + margin-right: 1.3%; + } + + .mosaic-item-text .mosaic-item-text-wrapper { + max-height: 120px; + /* Must be .mosaic-item absolute height (in px) */ + /* overflow hidden inherited */ + } } /* Helper classes to remove margin depending on the screen size */ @media (min-width: 768px) and (max-width: 992px) { .mosaic-group-item:nth-child(3n) { - margin-right: 0px; + margin-right: 0px; } } @media (min-width: 992px) and (max-width: 1200px) { .mosaic-group-item:nth-child(3n) { - margin-right: 0px; + margin-right: 0px; } } @media (min-width: 1200px) { .mosaic-group-item:nth-child(4n) { - margin-right: 0px; + margin-right: 0px; } } /****************/ @@ -854,30 +916,30 @@ table .group-actions { /* Filter brick */ /****************/ .tile.tile-filter-brick .tile_filterbox .form-group:first-child { - width: 100%; + width: 100%; } .tile.tile-filter-brick .tile_filterbox input[type="text"] { - width: 100%; + width: 100%; } @media (max-width: 768px) { .tile.tile-filter-brick .tile_filterbox .form-group:first-child { - margin-bottom: 5px; + margin-bottom: 5px; } } @media (min-width: 768px) { .tile.tile-filter-brick .tile_filterbox form { - display: table; + display: table; } .tile.tile-filter-brick .tile_filterbox .form-group:first-child { - display: table-cell; + display: table-cell; } .tile.tile-filter-brick .tile_filterbox button[type="submit"] { - margin-left: 5px; + margin-left: 5px; } } @media (min-width: 992px) { .tile.tile-filter-brick .tile_filterbox .form-group:first-child { - display: table-cell; + display: table-cell; } } /*********/ @@ -888,27 +950,27 @@ table .group-actions { position: relative; left: 3px; color: #ea7d1e; - font-size: 0.9em; + font-size: 0.9em; } /* ExternalKey */ .selectobject .input-group-addon { - cursor: pointer; + cursor: pointer; } /* InlineImage */ .inline-image { cursor: pointer; - cursor: zoom-in; + cursor: zoom-in; } /* CaseLog field */ .caselog_field_entry { border: 1px solid #ddd; - border-top: none; + border-top: none; } .caselog_field_entry_header { padding: 6px; font-size: 1em; border-bottom: 1px solid #fff; - background-color: #f2f2f2; + background-color: #f2f2f2; } .caselog_field_entry_button { display: block; @@ -919,89 +981,89 @@ table .group-actions { font-size: 16px; border: 1px solid #a6a6a6; border-bottom-color: #979797; - cursor: pointer; + cursor: pointer; } .caselog_field_entry_button:hover { - background-color: #ccc; + background-color: #ccc; } .caselog_field_entry_button:before { - content: "▴"; + content: "▴"; } .caselog_field_entry_button.collapsed:before { - content: "▾"; + content: "▾"; } .caselog_field_entry_content { margin: 10px; - overflow-x: auto; + overflow-x: auto; } /* LinkedSet*/ .form_linkedset_toggler, .form_linkedset_toggler:hover, .form_linkedset_toggler:focus { text-decoration: none; - color: inherit; + color: inherit; } .form_linkedset_toggler > .text { - margin-left: 0.4em; + margin-left: 0.4em; } .form_linkedset_toggler > .text:before { - content: "("; + content: "("; } .form_linkedset_toggler > .text:after { - content: ")"; + content: ")"; } .form_linkedset_toggler > .glyphicon { margin-left: 0.5em; font-size: 0.85em; color: #ea7d1e; - transition: transform 0.2s linear; + transition: transform 0.2s linear; } .form_linkedset_toggler > .glyphicon.collapsed { - transform: rotateZ(-90deg); + transform: rotateZ(-90deg); } /* - DataTables : Loader */ .form_linkedset_wrapper .datatables_overlay { - padding: 8px !important; + padding: 8px !important; } .form_linkedset_wrapper .overlay_content { - font-size: 0.6em; + font-size: 0.6em; } .form_linkedset_wrapper .content_loader { - margin: 0px; + margin: 0px; } .form_linkedset_wrapper .content_loader .icon { - height: 23px; + height: 23px; } /* - DataTables : Fit the table in the form */ .form_linkedset_wrapper .dataTables_wrapper { margin-bottom: 5px; - padding: 0px; + padding: 0px; } /* FileUpload */ .fileupload_field_content { padding: 8px 23px; border: 1px solid #ddd; - background-color: #f9f9f9; + background-color: #f9f9f9; } .fileupload_field_content > div { - margin-bottom: 15px; + margin-bottom: 15px; } .attachments_container .attachment { height: 95px; overflow-x: hidden; - text-align: center; + text-align: center; } .attachments_container .attachment:hover { - background-color: #e0e0e0; + background-color: #e0e0e0; } .attachments_container .attachment .attachment_name { overflow-x: hidden; text-overflow: ellipsis; - white-space: nowrap; + white-space: nowrap; } .attachments_container .attachment .btn { - margin-top: 3px; + margin-top: 3px; } .upload_container input { - display: inline; + display: inline; } .upload_container .loader { visibility: hidden; @@ -1010,24 +1072,24 @@ table .group-actions { animation: spin 1s linear infinite; -webkit-animation: spin 1s linear infinite; -moz-animation: spin 1s linear infinite; - -ms-animation: spin 1s linear infinite; + -ms-animation: spin 1s linear infinite; } #drag_overlay { display: block; top: inherit; bottom: 0px; - height: 0px; + height: 0px; } #drag_overlay .overlay_content { margin-top: 5em; width: 100%; - color: white; + color: white; } #drag_overlay .overlay_content .icon { - font-size: 3em; + font-size: 3em; } #drag_overlay .overlay_content .message { - font-size: 1.5em; + font-size: 1.5em; } /* Attachments drag & drop zone, only for none mobile devices */ @media (min-width: 768px) { @@ -1035,136 +1097,144 @@ table .group-actions { animation: show-drop-zone 0.3s ease-out forwards; -webkit-animation: show-drop-zone 0.3s ease-out forwards; -moz-animation: show-drop-zone 0.3s ease-out forwards; - -ms-animation: show-drop-zone 0.3s ease-out forwards; + -ms-animation: show-drop-zone 0.3s ease-out forwards; } #drag_overlay.drag_out { animation: hide-drop-zone 0.3s ease-out forwards; -webkit-animation: hide-drop-zone 0.3s ease-out forwards; -moz-animation: hide-drop-zone 0.3s ease-out forwards; - -ms-animation: hide-drop-zone 0.3s ease-out forwards; + -ms-animation: hide-drop-zone 0.3s ease-out forwards; } - @keyframes show-drop-zone { - 100% { - height: 20%; + + @keyframes show-drop-zone { + 100% { + height: 20%; + } } - } - @-webkit-keyframes show-drop-zone { - 100% { - height: 20%; + @-webkit-keyframes show-drop-zone { + 100% { + height: 20%; + } } - } - @-moz-keyframes show-drop-zone { - 100% { - height: 20%; + @-moz-keyframes show-drop-zone { + 100% { + height: 20%; + } } - } - @-ms-keyframes show-drop-zone { - 100% { - height: 20%; + @-ms-keyframes show-drop-zone { + 100% { + height: 20%; + } } - } - @keyframes hide-drop-zone { - 0% { - height: 20%; + @keyframes hide-drop-zone { + 0% { + height: 20%; + } + 100% { + height: 0%; + } } - 100% { - height: 0%; + @-webkit-keyframes hide-drop-zone { + 0% { + height: 20%; + } + 100% { + height: 0%; + } } - } - @-webkit-keyframes hide-drop-zone { - 0% { - height: 20%; + @-moz-keyframes hide-drop-zone { + 0% { + height: 20%; + } + 100% { + height: 0%; + } } - 100% { - height: 0%; + @-ms-keyframes hide-drop-zone { + 0% { + height: 20%; + } + 100% { + height: 0%; + } } - } - @-moz-keyframes hide-drop-zone { - 0% { - height: 20%; - } - 100% { - height: 0%; - } - } - @-ms-keyframes hide-drop-zone { - 0% { - height: 20%; - } - 100% { - height: 0%; - } - } } /* BlobField */ .form_fields .file_open_link { - margin-left: 10px; + margin-left: 10px; } .form_field .form-control-static img { max-width: 100% !important; - height: initial !important; + height: initial !important; } .form_buttons { padding-top: 20px; - text-align: center; + text-align: center; } .form_buttons .form_btn_misc { - margin-bottom: 20px; + margin-bottom: 20px; } .form_buttons .form_btn_transitions { - margin-bottom: 20px; + margin-bottom: 20px; } .form_buttons .btn .glyphicon { - margin-right: 0.5em; + margin-right: 0.5em; } .form_btn_regular.sticky { - display: none; + display: none; } @media (min-width: 768px) { /* Making regular button sticky */ - .form_buttons .form_btn_misc { - float: left !important; - } - .form_buttons .form_btn_transitions { - float: right !important; - margin-left: 3px; - } - .form_buttons .form_btn_regular { - text-align: right; - } - .form_buttons .form_btn_regular btn { - width: inherit; - } - .form_btn_regular.sticky { - display: block; - position: fixed; - bottom: 5em; - right: -2px; - /* TODO : SASS this to col-xs-12 padding */ - padding: 15px; - background-color: #fff; - border: 1px solid #ddd; - border-radius: 0px; - transition: right 0.3s; - } - .form_btn_regular.sticky.closed { - right: -75px; - } - .form_btn_regular.sticky button { - display: block; - } - .form_btn_regular.sticky button:first-child { - margin-bottom: 4px; - } + .form_buttons .form_btn_misc { + float: left !important; + } + + .form_buttons .form_btn_transitions { + float: right !important; + margin-left: 3px; + } + + .form_buttons .form_btn_regular { + text-align: right; + } + + .form_buttons .form_btn_regular btn { + width: inherit; + } + + .form_btn_regular.sticky { + display: block; + position: fixed; + bottom: 5em; + right: -2px; + /* TODO : SASS this to col-xs-12 padding */ + padding: 15px; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 0px; + transition: right 0.3s; + } + + .form_btn_regular.sticky.closed { + right: -75px; + } + + .form_btn_regular.sticky button { + display: block; + } + + .form_btn_regular.sticky button:first-child { + margin-bottom: 4px; + } } /* CKEditor : Adding BS error feedback */ .form_field div.cke { - border: 1px solid #ddd; + border: 1px solid #ddd; } .form_field.has-error div.cke { border: 1px solid #b94a48; border-radius: 0px; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); } /* CKEditor : Styling notifications based on BS alerts */ .cke_notification { @@ -1173,40 +1243,40 @@ table .group-actions { margin-bottom: 18px; border: 1px solid transparent; border-radius: 4px; - background-color: #fff; + background-color: #fff; } .cke_notification_close { position: absolute; top: 2px; - right: 5px; + right: 5px; } .cke_notification_message { margin-bottom: 0px; line-height: 1em; - font-size: 1em; + font-size: 1em; } .cke_notification_success { display: none; background-color: #dff0d8; border-color: #d6e9c6; - color: #468847; + color: #468847; } .cke_notification_warning { background-color: #fcf8e3; border-color: #fbeed5; - color: #c09853; + color: #c09853; } /* CKEditor : Misc */ .cke_toolbox_collapser, .cke_toolbox_collapser .cke_arrow { - cursor: pointer !important; + cursor: pointer !important; } /* DataTables : Selection inputs */ .dataTable.table th span.row_input, .dataTable.table td span.row_input { display: inline-block; width: 100%; - text-align: center; + text-align: center; } /* Wiki text (hyperlinks) */ .wiki_broken_link { - text-decoration: line-through; + text-decoration: line-through; } diff --git a/datamodels/2.x/itop-portal-base/portal/web/css/portal.scss b/datamodels/2.x/itop-portal-base/portal/web/css/portal.scss index f7d589b4a..1369d7d76 100644 --- a/datamodels/2.x/itop-portal-base/portal/web/css/portal.scss +++ b/datamodels/2.x/itop-portal-base/portal/web/css/portal.scss @@ -355,10 +355,65 @@ footer{ font-weight: bold; color: #333; } + +.home .tile .tile_title > span.icon { + color: $combodo-orange; +} + .home .tile .tile_description{ display: none; color: #555555; } + +.home div.tile-badge > a.tile { + display: table; + width: 100%; +} + +.home div.tile-badge > a.tile > div { + display: table-row; +} + +.home div.tile-badge > a.tile > div > div { + display: table-cell; +} + +.home div.tile-badge > a.tile > div > div.tile_decoration { + text-align: center; + vertical-align: top; + position: inherit; + float: inherit; +} + +.home div.tile-badge > a.tile > div > div.tile_body { + text-align: right; + padding-left: 0; +} + +.home div.tile-badge > a.tile > div > div.tile_body > div:first-child { + text-align: center; + margin: 0 auto; + margin-left: 10%; +} + +.home div.tile-badge > a.tile > div > div.tile_body > div:first-child > div { + text-align: center; +} + +.home div.tile-badge > a.tile > div > div.tile_body > div:first-child > div.tile_description { + margin-bottom: 1.5em; +} + +.home div.tile-badge > a.tile > div > div.tile_body > div:first-child > div.tile_title { + font-size: 1.5em; + font-weight: bold; +} + +.home div.tile-badge > a.tile > div > div.tile_body > div.tile_description { + text-align: right; +} + + @media (min-width: 768px) { .home .tile{ display: block; diff --git a/datamodels/2.x/itop-portal-base/portal/web/js/export.js b/datamodels/2.x/itop-portal-base/portal/web/js/export.js new file mode 100644 index 000000000..c11fd1ef2 --- /dev/null +++ b/datamodels/2.x/itop-portal-base/portal/web/js/export.js @@ -0,0 +1,109 @@ +// Copyright (c) 2010-2017 Combodo SARL +// +// This file is part of iTop. +// +// iTop is free software; you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// iTop is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with iTop. If not, see +// + +function ExportStartExport() { + var oParams = {}; + oParams.operation = 'export_build'; + oParams.format = sFormat; + oParams.expression = sOQL; + oParams.fields = sFields; + $.post(GetAbsoluteUrlAppRoot() + 'pages/ajax.render.php', oParams, function (data) { + if (data == null) { + ExportError('Export failed (no data provided), please contact your administrator'); + } + else { + ExportRun(data); + } + }, 'json') + .fail(function () { + ExportError('Export failed, please contact your administrator'); + }); +} + +function ExportError(sMessage) { + sDataState = 'error'; + $('#export-feedback').hide(); + $('#export-text-result').show(); + $('#export-error').html(sMessage); +} + +function ExportRun(data) { + switch (data.code) { + case 'run': + // Continue + $('.progress').progressbar({value: data.percentage}); + $('.export-message').html(data.message); + oParams = {}; + oParams.token = data.token; + if (sDataState == 'cancelled') { + oParams.operation = 'export_cancel'; + $('#export-cancel').hide(); + $('#export-close').show(); + } + else { + oParams.operation = 'export_build'; + } + + $.post(GetAbsoluteUrlAppRoot() + 'pages/ajax.render.php', oParams, function (data) { + ExportRun(data); + }, + 'json'); + break; + + case 'done': + sDataState = 'done'; + $('#export-cancel').hide(); + $('#export-close').show(); + $('.progress').progressbar({value: data.percentage}); + sMessage = '' + data.message + ''; + $('.export-message').html(sMessage); + if (data.text_result != undefined) { + if (data.mime_type == 'text/html') { + $('#export-content').parent().html(data.text_result); + $('#export-text-result').show(); + $('#export-text-result .listResults').tableHover(); + $('#export-text-result .listResults').tablesorter({widgets: ['myZebra']}); + } + else { + if ($('#export-text-result').closest('ui-dialog').length == 0) { + // not inside a dialog box, adjust the height... approximately + var jPane = $('#export-text-result').closest('.ui-layout-content'); + var iTotalHeight = jPane.height(); + jPane.children(':visible').each(function () { + if ($(this).attr('id') != '') { + iTotalHeight -= $(this).height(); + } + }); + $('#export-content').height(iTotalHeight - 80); + } + $('#export-content').val(data.text_result); + $('#export-text-result').show(); + } + } + break; + + case 'error': + sDataState = 'error'; + $('#export-feedback').hide(); + $('#export-text-result').show(); + $('#export-error').html(data.message); + $('#export-cancel').hide(); + $('#export-close').show(); + default: + } +}