N°3294 - Display and refresh counters in OQL menu entries

This commit is contained in:
Eric
2020-10-27 15:11:30 +01:00
parent 48b3716278
commit 8502fa7721
7 changed files with 484 additions and 378 deletions

View File

@@ -200,6 +200,49 @@ class ApplicationMenu
return self::$aMenusIndex;
}
/**
* Get entries count for all the menus
*
* @param array $aExtraParams
*
* @return array
* @throws \DictExceptionMissingString
* @since 3.0.0
*/
public static function GetMenusCount($aExtraParams = array())
{
$aMenuGroups = static::GetMenuGroups($aExtraParams);
$aMenusCount = [];
foreach ($aMenuGroups as $aMenuGroup) {
$aSubMenuNodes = $aMenuGroup['aSubMenuNodes'];
$aMenusCount = array_merge($aMenusCount, static::GetSubMenusCount($aSubMenuNodes));
}
return $aMenusCount;
}
/**
* Recurse sub menus for counts
*
* @param array $aSubMenuNodes
*
* @return array
* @since 3.0.0
*/
private static function GetSubMenusCount(array $aSubMenuNodes)
{
$aSubMenusCount = [];
foreach ($aSubMenuNodes as $aSubMenuNode) {
if ($aSubMenuNode['bHasCount']) {
$oMenuNode = static::GetMenuNode(static::GetMenuIndexById($aSubMenuNode['sId']));
$aSubMenusCount[$aSubMenuNode['sId']] = $oMenuNode->GetEntriesCount();
}
$aSubMenusCount = array_merge($aSubMenusCount, static::GetSubMenusCount($aSubMenuNode['aSubMenuNodes']));
}
return $aSubMenusCount;
}
/**
* Return an array of menu groups
*
@@ -277,7 +320,7 @@ class ApplicationMenu
$aSubMenuNodes[] = [
'sId' => $oSubMenuNode->GetMenuId(),
'sTitle' => $oSubMenuNode->GetTitle(),
'sEntriesCount' => $oSubMenuNode->GetEntriesCount(),
'bHasCount' => $oSubMenuNode->HasCount(),
'sUrl' => $oSubMenuNode->GetHyperlink($aExtraParams),
'bOpenInNewWindow' => $oSubMenuNode->IsHyperLinkInNewWindow(),
'aSubMenuNodes' => static::GetSubMenuNodes($sSubMenuItemIdx, $aExtraParams),
@@ -667,9 +710,26 @@ abstract class MenuNode
return Dict::S("Menu:$this->sMenuId", str_replace('_', ' ', $this->sMenuId));
}
/**
* Indicates if the page corresponding to this menu node is countable
*
* @return bool true if corresponding page is countable
* @since 3.0.0
*/
public function HasCount()
{
return false;
}
/**
* Get the number of entries of the page corresponding to this menu item.
*
* @return int the number of entries
* @since 3.0.0
*/
public function GetEntriesCount()
{
return -1;
return 0;
}
/**
@@ -1083,6 +1143,11 @@ class OQLMenuNode extends MenuNode
}
}
public function HasCount()
{
return true;
}
public function GetEntriesCount()
{
// Count the entries up to 99

File diff suppressed because it is too large Load Diff

View File

@@ -23,78 +23,76 @@ $(function()
{
// default options
options:
{
active_menu_group: null,
filter_keyup_throttle: 200, // In milliseconds
},
{
active_menu_group: null,
display_counts: false,
filter_keyup_throttle: 200, // In milliseconds
},
css_classes:
{
menu_expanded: 'ibo-is-expanded',
menu_active: 'ibo-is-active',
menu_filtered: 'ibo-is-filtered',
menu_group_active: 'ibo-is-active',
menu_nodes_active: 'ibo-is-active'
},
{
menu_expanded: 'ibo-is-expanded',
menu_active: 'ibo-is-active',
menu_filtered: 'ibo-is-filtered',
menu_group_active: 'ibo-is-active',
menu_nodes_active: 'ibo-is-active'
},
js_selectors:
{
menu_toggler: '[data-role="ibo-navigation-menu--toggler"]',
menu_group: '[data-role="ibo-navigation-menu--menu-group"]',
menu_drawer: '[data-role="ibo-navigation-menu--drawer"]',
menu_filter_input: '[data-role="ibo-navigation-menu--menu-filter-input"]',
menu_filter_clear: '[data-role="ibo-navigation-menu--menu-filter-clear"]',
user_menu_toggler: '[data-role="ibo-navigation-menu--user-menu--toggler"]',
user_menu_container: '[data-role="ibo-navigation-menu--user-menu-container"]',
user_menu: '[data-role="ibo-navigation-menu--user-menu-container"] > [data-role="ibo-popover-menu"]'
},
{
menu_toggler: '[data-role="ibo-navigation-menu--toggler"]',
menu_group: '[data-role="ibo-navigation-menu--menu-group"]',
menu_drawer: '[data-role="ibo-navigation-menu--drawer"]',
menu_filter_input: '[data-role="ibo-navigation-menu--menu-filter-input"]',
menu_filter_clear: '[data-role="ibo-navigation-menu--menu-filter-clear"]',
user_menu_toggler: '[data-role="ibo-navigation-menu--user-menu--toggler"]',
user_menu_container: '[data-role="ibo-navigation-menu--user-menu-container"]',
user_menu: '[data-role="ibo-navigation-menu--user-menu-container"] > [data-role="ibo-popover-menu"]'
},
filter_throttle_timeout: null,
// the constructor
_create: function()
{
_create: function () {
this.element.addClass('ibo-navigation-menu');
$(this.js_selectors.user_menu).popover_menu({'toggler': this.js_selectors.user_menu_toggler});
this._bindEvents();
},
// events bound via _bind are removed automatically
// revert other modifications here
_destroy: function()
{
_destroy: function () {
this.element.removeClass('ibo-navigation-menu');
},
_bindEvents: function()
{
_bindEvents: function () {
const me = this;
const oBodyElem = $('body');
// Click on collapse/expand toggler
this.element.find(this.js_selectors.menu_toggler).on('click', function(oEvent){
this.element.find(this.js_selectors.menu_toggler).on('click', function (oEvent) {
me._onTogglerClick(oEvent);
});
// Click on menu group
this.element.find(this.js_selectors.menu_group).on('click', function(oEvent){
this.element.find(this.js_selectors.menu_group).on('click', function (oEvent) {
me._onMenuGroupClick(oEvent, $(this))
});
// Mostly for outside clicks that should close elements
oBodyElem.on('click', function(oEvent){
oBodyElem.on('click', function (oEvent) {
me._onBodyClick(oEvent);
});
// Mostly for hotkeys
oBodyElem.on('keyup', function(oEvent){
oBodyElem.on('keyup', function (oEvent) {
me._onBodyKeyUp(oEvent);
});
// Menus filter
// - Input itself
this.element.find(this.js_selectors.menu_filter_input).on('keyup', function(oEvent){
this.element.find(this.js_selectors.menu_filter_input).on('keyup', function (oEvent) {
me._onFilterKeyUp(oEvent);
});
// - Clear icon
this.element.find(this.js_selectors.menu_filter_clear).on('click', function(oEvent){
this.element.find(this.js_selectors.menu_filter_clear).on('click', function (oEvent) {
me._onFilterClearClick(oEvent);
})
// User info
this.element.find(this.js_selectors.user_menu_toggler).on('click', function(oEvent){
this.element.find(this.js_selectors.user_menu_toggler).on('click', function (oEvent) {
me._onUserMenuTogglerClick(oEvent);
});
},
@@ -133,8 +131,6 @@ $(function()
// on every single key up in the application, which might not be what we want... (time consuming)
if((oEvent.altKey === true) && (oEvent.key === 'm' || oEvent.key === 'M'))
{
const me = this;
if(this._getActiveMenuGroupId() === null)
{
const sFirstMenuGroupId = this.element.find(this.js_selectors.menu_group+':first').attr('data-menu-group-id');
@@ -159,12 +155,12 @@ $(function()
{
this._clearFiltering();
}
else
{
else {
// Reset throttle timeout on key stroke
clearTimeout(this.filter_throttle_timeout);
this.filter_throttle_timeout = setTimeout(function(){
this.filter_throttle_timeout = setTimeout(function () {
me._doFiltering(sValue);
me.refreshCounts();
}, this.options.filter_keyup_throttle);
}
},
@@ -184,7 +180,7 @@ $(function()
oEvent.preventDefault();
var oEventTarget = $(oEvent.target);
var aEventTargetPos = oEventTarget.position();
$(this.js_selectors.user_menu_container).css({
'top': (aEventTargetPos.top + parseInt(oEventTarget.css('marginTop'), 10) - $(this.js_selectors.user_menu).height()) + 'px',
'left': (aEventTargetPos.left + parseInt(oEventTarget.css('marginLeft'), 10) + oEventTarget.width()) + 'px'
@@ -235,15 +231,15 @@ $(function()
* @param sMenuGroupId string
* @private
*/
_openDrawer: function(sMenuGroupId)
{
_openDrawer: function (sMenuGroupId) {
this.refreshCounts();
this._clearActiveMenuGroup();
// Note: This causes the filter to be cleared event when using the hotkey to reopen a previously filled filter
this._clearFiltering();
// Set new active group
this.element.find('[data-role="ibo-navigation-menu--menu-group"][data-menu-group-id="'+sMenuGroupId+'"]').addClass(this.css_classes.menu_group_active);
this.element.find('[data-role="ibo-navigation-menu--menu-nodes"][data-menu-group-id="'+sMenuGroupId+'"]').addClass(this.css_classes.menu_nodes_active);
this.element.find('[data-role="ibo-navigation-menu--menu-group"][data-menu-group-id="' + sMenuGroupId + '"]').addClass(this.css_classes.menu_group_active);
this.element.find('[data-role="ibo-navigation-menu--menu-nodes"][data-menu-group-id="' + sMenuGroupId + '"]').addClass(this.css_classes.menu_nodes_active);
// Set menu as active
this.element.addClass(this.css_classes.menu_active);
@@ -252,8 +248,7 @@ $(function()
* Close the drawer after clearing the active menu group
* @private
*/
_closeDrawer: function()
{
_closeDrawer: function () {
this._clearActiveMenuGroup();
// Set menu as non active
@@ -261,8 +256,7 @@ $(function()
},
// Menus filter methods
_focusFilter: function()
{
_focusFilter: function () {
this.element.find(this.js_selectors.menu_filter_input)
.trigger('click')
.trigger('focus');
@@ -271,8 +265,7 @@ $(function()
* Remove the current filter value and reset the menu nodes display
* @private
*/
_clearFiltering: function()
{
_clearFiltering: function () {
this.element.find(this.js_selectors.menu_filter_input).val('');
// Reset display of everything
@@ -301,22 +294,19 @@ $(function()
this.element.find('[data-role="ibo-navigation-menu--menu-node"]').hide();
// Show matching menu node
this.element.find('[data-role="ibo-navigation-menu--menu-node"]').each(function(){
this.element.find('[data-role="ibo-navigation-menu--menu-node"]').each(function () {
const sNodeValue = me._formatValueForFilterComparison($(this).children('[data-role="ibo-navigation-menu--menu-node-title"]:first').text());
let bMatches = true;
// On first non matching part, we consider that the menu node is not a match
for(let iIdx in aFilterValueParts)
{
if(sNodeValue.indexOf(aFilterValueParts[iIdx]) === -1)
{
for (let iIdx in aFilterValueParts) {
if (sNodeValue.indexOf(aFilterValueParts[iIdx]) === -1) {
bMatches = false;
break;
}
}
if(bMatches)
{
if (bMatches) {
// Note: Selector must be recursive
$(this).parents('[data-role="ibo-navigation-menu--menu-nodes"], [data-role="ibo-navigation-menu--menu-node"]').show();
$(this).show();
@@ -330,9 +320,33 @@ $(function()
* @returns string
* @private
*/
_formatValueForFilterComparison: function(sOriginalValue)
{
_formatValueForFilterComparison: function (sOriginalValue) {
return sOriginalValue.toLowerCase().latinise();
},
/**
* Refresh count badges for OQL menus
*/
refreshCounts: function () {
const me = this;
if (this.options.display_counts) {
$.ajax({
method: "POST",
url: GetAbsoluteUrlAppRoot() + 'pages/ajax.render.php',
data: {
operation: "get_menus_count"
},
dataType: "json"
})
.done(function (data) {
if (data.code === "done") {
for (const [key, value] of Object.entries(data.counts)) {
let menuEntry = me.element.find('[data-menu-id="' + key + '"]');
menuEntry.html(value);
menuEntry.show();
}
}
});
}
}
});
});

View File

@@ -77,8 +77,7 @@ try
// N°2780 Fix ContextTag for console
// some operations are also used in the portal though
switch ($operation)
{
switch ($operation) {
case 'export_build_portal':
case 'export_download':
// do nothing : used in portal (export.js in portal-base)
@@ -88,25 +87,21 @@ try
ContextTag::AddContext(ContextTag::TAG_CONSOLE);
}
switch ($operation)
{
$oAjaxRenderController = new AjaxRenderController();
switch ($operation) {
case 'datatable':
case 'pagination':
$oPage->SetContentType('text/html');
$extraParams = utils::ReadParam('extra_param', '', false, 'raw_data');
$aExtraParams = array();
if (is_array($extraParams))
{
if (is_array($extraParams)) {
$aExtraParams = $extraParams;
}
else
{
} else {
$sExtraParams = stripslashes($extraParams);
if (!empty($sExtraParams))
{
if (!empty($sExtraParams)) {
$val = json_decode(str_replace("'", '"', $sExtraParams), true /* associative array */);
if ($val !== null)
{
if ($val !== null) {
$aExtraParams = $val;
}
}
@@ -2448,11 +2443,11 @@ EOF
break;
case 'export_build':
AjaxRenderController::ExportBuild($oPage, false);
$oAjaxRenderController->ExportBuild($oPage, false);
break;
case 'export_build_portal':
AjaxRenderController::ExportBuild($oPage, true);
$oAjaxRenderController->ExportBuild($oPage, true);
break;
case 'export_download':
@@ -2806,6 +2801,10 @@ EOF
case 'new_entry_group':
break;
case 'get_menus_count':
$oAjaxRenderController->GetMenusCount($oPage);
break;
default:
$oPage->p("Invalid query.");
}

View File

@@ -6,7 +6,8 @@
namespace Combodo\iTop\Controller;
use ajax_page;
use AjaxPage;
use ApplicationMenu;
use BulkExport;
use BulkExportException;
use DBObjectSearch;
@@ -18,13 +19,13 @@ use utils;
class AjaxRenderController
{
/**
* @param \ajax_page $oPage
* @param \AjaxPage $oPage
*
* @param bool $bTokenOnly
*
* @throws \Exception
*/
public static function ExportBuild(ajax_page $oPage, bool $bTokenOnly)
public function ExportBuild(AjaxPage $oPage, bool $bTokenOnly)
{
register_shutdown_function(function () {
$aErr = error_get_last();
@@ -113,4 +114,19 @@ class AjaxRenderController
$oPage->add(json_encode($aResult));
}
}
/**
* Get all the menus count
*
* The resulting JSON is added to the page with the format:
* {"code": "done or error", "counts": {"menu_id_1": count1, "menu_id_2": count2...}}
*
* @param \AjaxPage $oPage
*/
public function GetMenusCount(AjaxPage $oPage)
{
$aCounts = ApplicationMenu::GetMenusCount();
$aResult = ['code' => 'done', 'counts' => $aCounts];
$oPage->add(json_encode($aResult));
}
}

View File

@@ -1 +1,5 @@
$('#{{ oUIBlock.GetId() }}').navigation_menu();
$('#{{ oUIBlock.GetId() }}').navigation_menu({
display_counts: {% if get_config_parameter('display_menus_count') %} true {% else %} false {% endif %}
});
$('#{{ oUIBlock.GetId() }}').navigation_menu('refreshCounts', null);

View File

@@ -1,8 +1,8 @@
<li class="ibo-navigation-menu--menu-node" data-role="ibo-navigation-menu--menu-node" data-menu-node-id="{{ aMenuNode.sId }}">
{% if aMenuNode.sUrl is not empty %}
{% set sTarget = (aMenuNode.bOpenInNewWindow == true) ? 'target="_blank"' : '' %}
{% if aMenuNode.sEntriesCount != "-1" %}
<a class="ibo-navigation-menu--menu-node-title" data-role="ibo-navigation-menu--menu-node-title" href="{{ aMenuNode.sUrl }}" {{ sTarget|raw }}>{{ aMenuNode.sTitle }}<span class="ibo-navigation-menu--menu-counter">{{ aMenuNode.sEntriesCount }}</span></a>
{% if aMenuNode.bHasCount %}
<a class="ibo-navigation-menu--menu-node-title" data-role="ibo-navigation-menu--menu-node-title" href="{{ aMenuNode.sUrl }}" {{ sTarget|raw }}>{{ aMenuNode.sTitle }}<span class="ibo-navigation-menu--menu-counter" data-menu-id="{{ aMenuNode.sId }}" style="display: none;"></span></a>
{% else %}
<a class="ibo-navigation-menu--menu-node-title" data-role="ibo-navigation-menu--menu-node-title" href="{{ aMenuNode.sUrl }}" {{ sTarget|raw }}>{{ aMenuNode.sTitle }}</a>
{% endif %}