mirror of
https://github.com/Combodo/iTop.git
synced 2026-07-17 12:16:40 +02:00
Compare commits
9 Commits
faf/faf-da
...
feature/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c7ca024ef | ||
|
|
ca4a01f49b | ||
|
|
ba405e64d2 | ||
|
|
55444eaacd | ||
|
|
bb4e1cf019 | ||
|
|
284b017c8a | ||
|
|
2d39cc6e93 | ||
|
|
7f9f9a528e | ||
|
|
5a25554f4e |
@@ -878,6 +878,41 @@ class DashletObjectList extends Dashlet
|
||||
$this->aProperties['title'] = '';
|
||||
$this->aProperties['query'] = 'SELECT Contact';
|
||||
$this->aProperties['menu'] = false;
|
||||
$this->aProperties['tree_grouping_attr'] = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all attribute codes allowed for tree grouping for the class targeted by $sOql.
|
||||
* Which means any external / hierarchical keys of the same class (or child classes)
|
||||
*
|
||||
* @param string $sOql
|
||||
* @return array [attcode => label]
|
||||
*/
|
||||
protected function GetTreeGroupingAttributes(string $sOql): array
|
||||
{
|
||||
$aAttrs = [];
|
||||
try {
|
||||
$oQuery = $this->oModelReflection->GetQuery($sOql);
|
||||
$sClass = $oQuery->GetClass();
|
||||
foreach ($this->oModelReflection->ListAttributes($sClass) as $sAttCode => $sAttType) {
|
||||
// Ignore non-external key attributes
|
||||
if (false === is_a($sAttType, AttributeExternalKey::class, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore external that do not point to the same class
|
||||
$sExtKeyTargetClass = $this->oModelReflection->GetAttributeProperty($sClass, $sAttCode, 'targetclass', '');
|
||||
if (false === is_a($sExtKeyTargetClass, $sClass, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$aAttrs[$sAttCode] = $this->oModelReflection->GetLabel($sClass, $sAttCode);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Fallback: return empty list on OQL error
|
||||
}
|
||||
|
||||
return $aAttrs;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -905,6 +940,9 @@ class DashletObjectList extends Dashlet
|
||||
"panel_title" => Dict::S($sTitle),
|
||||
"panel_class" => $sClass,
|
||||
];
|
||||
if (utils::IsNotNullOrEmptyString($this->aProperties['tree_grouping_attr'])) {
|
||||
$aParams['tree_grouping_attr'] = $this->aProperties['tree_grouping_attr'];
|
||||
}
|
||||
$sBlockId = 'block_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occurring in the same DOM)
|
||||
//$oBlock->DisplayIntoContentBlock($oPanel, $oPage, $sBlockId, array_merge($aExtraParams, $aParams));
|
||||
|
||||
@@ -965,6 +1003,30 @@ HTML;
|
||||
return DBObjectSearch::FromOQL($sQuery, $aQueryParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function Update($aValues, $aUpdatedFields)
|
||||
{
|
||||
if (in_array('query', $aUpdatedFields)) {
|
||||
try {
|
||||
$oCurrSearch = $this->oModelReflection->GetQuery($aValues['query']);
|
||||
$sCurrClass = $oCurrSearch->GetClass();
|
||||
|
||||
$oPrevSearch = $this->oModelReflection->GetQuery($this->aProperties['query']);
|
||||
$sPrevClass = $oPrevSearch->GetClass();
|
||||
|
||||
if ($sCurrClass !== $sPrevClass) {
|
||||
$this->aProperties['tree_grouping_attr'] = '';
|
||||
$this->bFormRedrawNeeded = true;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->bFormRedrawNeeded = true;
|
||||
}
|
||||
}
|
||||
return parent::Update($aValues, $aUpdatedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
@@ -979,6 +1041,15 @@ HTML;
|
||||
$oField->AddCSSClass("ibo-is-code");
|
||||
$oForm->AddField($oField);
|
||||
|
||||
$aTreeGroupingAttrs = $this->GetTreeGroupingAttributes($this->aProperties['query']);
|
||||
$oField = new DesignerComboField(
|
||||
'tree_grouping_attr',
|
||||
Dict::S('UI:DashletObjectList:Prop-TreeGroupingAttr'),
|
||||
$this->aProperties['tree_grouping_attr']
|
||||
);
|
||||
$oField->SetAllowedValues($aTreeGroupingAttrs);
|
||||
$oForm->AddField($oField);
|
||||
|
||||
$oField = new DesignerBooleanField('menu', Dict::S('UI:DashletObjectList:Prop-Menu'), $this->aProperties['menu']);
|
||||
$oForm->AddField($oField);
|
||||
}
|
||||
@@ -1017,6 +1088,16 @@ HTML;
|
||||
$oField->AddCSSClass("ibo-is-code");
|
||||
$oForm->AddField($oField);
|
||||
|
||||
// TODO: Add tree grouping attr. But what is this for?
|
||||
$aTreeGroupingAttrs = $this->GetTreeGroupingAttributes($this->aProperties['query']);
|
||||
$oField = new DesignerComboField(
|
||||
'tree_grouping_attr',
|
||||
Dict::S('UI:DashletObjectList:Prop-TreeGroupingAttr'),
|
||||
$this->aProperties['tree_grouping_attr']
|
||||
);
|
||||
$oField->SetAllowedValues($aTreeGroupingAttrs);
|
||||
$oForm->AddField($oField);
|
||||
|
||||
$oField = new DesignerBooleanField('menu', Dict::S('UI:DashletObjectList:Prop-Menu'), $this->aProperties['menu']);
|
||||
$oForm->AddField($oField);
|
||||
}
|
||||
|
||||
@@ -1264,6 +1264,7 @@ When associated with a trigger, each action is given an "order" number, specifyi
|
||||
'UI:DashletObjectList:Prop-Title' => 'Title',
|
||||
'UI:DashletObjectList:Prop-Query' => 'Query',
|
||||
'UI:DashletObjectList:Prop-Menu' => 'Menu',
|
||||
'UI:DashletObjectList:Prop-TreeGroupingAttr' => 'Tree group by',
|
||||
|
||||
'UI:DashletGroupBy:Prop-Title' => 'Title',
|
||||
'UI:DashletGroupBy:Prop-Query' => 'Query',
|
||||
|
||||
@@ -1217,6 +1217,7 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé
|
||||
'UI:DashletObjectList:Prop-Title' => 'Titre',
|
||||
'UI:DashletObjectList:Prop-Query' => 'Requête OQL',
|
||||
'UI:DashletObjectList:Prop-Menu' => 'Menu',
|
||||
'UI:DashletObjectList:Prop-TreeGroupingAttr' => 'Arborescence par',
|
||||
'UI:DashletGroupBy:Prop-Title' => 'Titre',
|
||||
'UI:DashletGroupBy:Prop-Query' => 'Requête OQL',
|
||||
'UI:DashletGroupBy:Prop-Style' => 'Style',
|
||||
|
||||
322
js/dataTables.tree-grouping.js
Normal file
322
js/dataTables.tree-grouping.js
Normal file
@@ -0,0 +1,322 @@
|
||||
/**
|
||||
* Tree grouping for iTop DataTables.
|
||||
*
|
||||
* Activated when a DataTable has the 'sTreeGroupingAttr' option set.
|
||||
* Transforms a flat list of objects into an expandable/collapsible tree grouped by parent ID.
|
||||
*
|
||||
* @copyright Copyright (C) 2010-2024 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
var iTopDataTableTreeGrouping = (function ($) {
|
||||
'use strict';
|
||||
|
||||
// Inject CSS once
|
||||
var bCssInjected = false;
|
||||
function _injectCss() {
|
||||
if (bCssInjected) {
|
||||
return;
|
||||
}
|
||||
bCssInjected = true;
|
||||
$('<style>')
|
||||
.prop('type', 'text/css')
|
||||
.html(
|
||||
'.ibo-datatable--tree-toggle,' +
|
||||
'.ibo-datatable--tree-leaf {' +
|
||||
' display: inline-block;' +
|
||||
' width: 16px;' +
|
||||
' margin-right: 4px;' +
|
||||
' text-align: center;' +
|
||||
'}' +
|
||||
'.ibo-datatable--tree-toggle {' +
|
||||
' cursor: pointer;' +
|
||||
' color: #666;' +
|
||||
'}' +
|
||||
'.ibo-datatable--tree-toggle:hover {' +
|
||||
' color: #333;' +
|
||||
'}' +
|
||||
'.ibo-datatable--tree-spacer {' +
|
||||
' display: inline-block;' +
|
||||
'}' +
|
||||
'.dataTables_scrollHead th.ibo-datatable--tree-sortable {' +
|
||||
' cursor: pointer;' +
|
||||
' user-select: none;' +
|
||||
'}' +
|
||||
'.dataTables_scrollHead th.ibo-datatable--tree-sortable::after {' +
|
||||
' font-family: "Font Awesome 5 Free";' +
|
||||
' font-weight: 900;' +
|
||||
' content: "\\f0dc";' +
|
||||
' margin-left: 6px;' +
|
||||
' opacity: 0.4;' +
|
||||
' font-size: 0.85em;' +
|
||||
'}' +
|
||||
'.dataTables_scrollHead th.ibo-datatable--tree-sort-asc::after {' +
|
||||
' content: "\\f0de";' +
|
||||
' opacity: 1;' +
|
||||
'}' +
|
||||
'.dataTables_scrollHead th.ibo-datatable--tree-sort-desc::after {' +
|
||||
' content: "\\f0dd";' +
|
||||
' opacity: 1;' +
|
||||
'}' +
|
||||
/* Suppress ::after on the minimised thead clone that DataTables injects
|
||||
into scrollBody on every draw — it copies scrollHead classes verbatim,
|
||||
so our sort-icon classes would show there too without this rule. */
|
||||
'.dataTables_scrollBody thead th::after {' +
|
||||
' display: none !important;' +
|
||||
'}'
|
||||
)
|
||||
.appendTo('head');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build node map and parent-child relationships from flat data.
|
||||
*
|
||||
* @param {Array} aData - flat row array (each item must have an 'id' field)
|
||||
* @param {string} sParentKey - data key that holds the parent object ID (e.g. "MyClass/parent_id/raw")
|
||||
* @returns {{roots: Array, nodeMap: Object}}
|
||||
*/
|
||||
function _buildTree(aData, sParentKey) {
|
||||
var oNodeMap = {};
|
||||
var aRoots = [];
|
||||
|
||||
// First pass: create a node for every row
|
||||
aData.forEach(function (oRow) {
|
||||
oNodeMap[String(oRow.id)] = $.extend(true, {}, oRow, {
|
||||
_itop_children: [],
|
||||
_itop_has_children: false,
|
||||
_itop_parent_node_id: null,
|
||||
_itop_depth: 0
|
||||
});
|
||||
});
|
||||
|
||||
// Second pass: wire parent ↔ child links
|
||||
aData.forEach(function (oRow) {
|
||||
var sId = String(oRow.id);
|
||||
var sParentId = String(oRow[sParentKey] || '');
|
||||
|
||||
if (sParentId && sParentId !== '0' && sParentId !== sId && oNodeMap[sParentId]) {
|
||||
oNodeMap[sParentId]._itop_children.push(oNodeMap[sId]);
|
||||
oNodeMap[sParentId]._itop_has_children = true;
|
||||
oNodeMap[sId]._itop_parent_node_id = sParentId;
|
||||
} else {
|
||||
aRoots.push(oNodeMap[sId]);
|
||||
}
|
||||
});
|
||||
|
||||
return {roots: aRoots, nodeMap: oNodeMap};
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten the tree in depth-first order, preserving the server-side sort order of siblings.
|
||||
*
|
||||
* @param {Array} aNodes - nodes at the current level (roots or children)
|
||||
* @param {number} iDepth - depth of these nodes (0 = root)
|
||||
* @returns {Array} - flat list in display order
|
||||
*/
|
||||
function _flattenTree(aNodes, iDepth) {
|
||||
var aResult = [];
|
||||
aNodes.forEach(function (oNode) {
|
||||
oNode._itop_depth = iDepth;
|
||||
aResult.push(oNode);
|
||||
if (oNode._itop_children.length > 0) {
|
||||
aResult = aResult.concat(_flattenTree(oNode._itop_children, iDepth + 1));
|
||||
}
|
||||
});
|
||||
return aResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the sorted flat list to hide descendants of collapsed nodes.
|
||||
*
|
||||
* @param {Array} aSortedData - full DFS-ordered flat list
|
||||
* @param {Object} oNodeMap - {id → node} map
|
||||
* @param {Set} oCollapsed - set of collapsed node IDs
|
||||
* @returns {Array}
|
||||
*/
|
||||
function _getVisibleData(aSortedData, oNodeMap, oCollapsed) {
|
||||
return aSortedData.filter(function (oRow) {
|
||||
var sParentId = oRow._itop_parent_node_id;
|
||||
while (sParentId) {
|
||||
if (oCollapsed.has(sParentId)) {
|
||||
return false;
|
||||
}
|
||||
var oParent = oNodeMap[sParentId];
|
||||
sParentId = oParent ? oParent._itop_parent_node_id : null;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize hierarchical mode for an already-created DataTable.
|
||||
*
|
||||
* The DataTable must have been created with serverSide:false, paging:false,
|
||||
* ordering:false, and data:[]. This function populates it with tree-structured
|
||||
* data and wires up collapse/expand and column-sort interactions.
|
||||
*
|
||||
* @param {string} sTableId - DOM id of the <table> element (without #)
|
||||
* @param {DataTables.Api} oDt - DataTables API instance
|
||||
* @param {Object} oConfig
|
||||
* @param {string} oConfig.parentKey - data key for parent ID
|
||||
* (e.g. "MyClass/parent_id/raw")
|
||||
* @param {string} oConfig.ajaxUrl - URL used to refresh data
|
||||
* @param {Object} oConfig.ajaxData - POST params for the search operation
|
||||
* @param {Object} oConfig.initData - pre-loaded server data
|
||||
* ({draw, recordsTotal, data:[…]})
|
||||
*/
|
||||
function setup(sTableId, oDt, oConfig) {
|
||||
_injectCss();
|
||||
|
||||
var oState = {
|
||||
allData: [],
|
||||
sortedData: [],
|
||||
nodeMap: {},
|
||||
collapsed: new Set(),
|
||||
sortColIndex: -1,
|
||||
sortDir: 'asc'
|
||||
};
|
||||
|
||||
// ── Rebuild tree from oState.allData ──────────────────────────────────────
|
||||
function _rebuild() {
|
||||
var oTree = _buildTree(oState.allData, oConfig.parentKey);
|
||||
oState.nodeMap = oTree.nodeMap;
|
||||
oState.sortedData = _flattenTree(oTree.roots, 0);
|
||||
}
|
||||
|
||||
// ── Fetch sorted data from server and rebuild tree ────────────────────────
|
||||
// bResetCollapsed: true resets expanded/collapsed state (use for full refresh),
|
||||
// false preserves it (use for sort-only re-fetch).
|
||||
function _fetchData(bResetCollapsed) {
|
||||
var oParams = $.extend({}, oConfig.ajaxData, {start: 0, end: 0, draw: 1});
|
||||
if (oState.sortColIndex >= 0) {
|
||||
oParams.order = [{column: oState.sortColIndex, dir: oState.sortDir}];
|
||||
}
|
||||
$('#' + sTableId).closest('.dataTables_wrapper').block({
|
||||
message: '<i class="fa fa-sync-alt fa-spin fa-fw"></i>',
|
||||
css: {border: '0px'}
|
||||
});
|
||||
$.post(oConfig.ajaxUrl, oParams)
|
||||
.done(function (oResponse) {
|
||||
oState.allData = (oResponse && oResponse.data) ? oResponse.data : [];
|
||||
if (bResetCollapsed) {
|
||||
oState.collapsed = new Set();
|
||||
}
|
||||
_rebuild();
|
||||
_redraw();
|
||||
});
|
||||
}
|
||||
|
||||
// ── Update DataTable with currently visible rows ───────────────────────────
|
||||
function _redraw() {
|
||||
var aVisible = _getVisibleData(oState.sortedData, oState.nodeMap, oState.collapsed);
|
||||
oDt.clear();
|
||||
oDt.rows.add(aVisible);
|
||||
oDt.draw(false);
|
||||
}
|
||||
|
||||
// ── Add indent spacer + toggle button to the first cell of each row ───────
|
||||
function _applyTreeUi() {
|
||||
oDt.rows().every(function () {
|
||||
var oTr = this.node();
|
||||
var oData = this.data();
|
||||
if (!oTr || $(oTr).hasClass('ibo-datatable--tree-processed')) {
|
||||
return;
|
||||
}
|
||||
$(oTr).addClass('ibo-datatable--tree-processed');
|
||||
|
||||
var iDepth = oData._itop_depth || 0;
|
||||
var sNodeId = String(oData.id);
|
||||
var $firstCell = $(oTr).find('td').eq(0);
|
||||
|
||||
// Indentation spacer
|
||||
var $spacer = $('<span class="ibo-datatable--tree-spacer"></span>')
|
||||
.css('width', (iDepth * 20) + 'px');
|
||||
|
||||
if (oData._itop_has_children) {
|
||||
var bCollapsed = oState.collapsed.has(sNodeId);
|
||||
var $toggle = $('<span></span>')
|
||||
.addClass('ibo-datatable--tree-toggle fas ' + (bCollapsed ? 'fa-caret-right' : 'fa-caret-down'))
|
||||
.attr('data-node-id', sNodeId);
|
||||
$firstCell.prepend($toggle).prepend($spacer);
|
||||
} else {
|
||||
$firstCell.prepend($('<span class="ibo-datatable--tree-leaf"></span>')).prepend($spacer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Attach sort click handlers to visible column headers ──────────────────
|
||||
function _attachSortHandlers() {
|
||||
// DataTables with scrollX duplicates the header; the user sees/clicks the
|
||||
// copy in .dataTables_scrollHead. Attach there first, fall back to the
|
||||
// table's own <thead> if no wrapper exists.
|
||||
var $wrapper = $('#' + sTableId).closest('.dataTables_wrapper');
|
||||
var $ths = $wrapper.find('.dataTables_scrollHead thead th');
|
||||
if ($ths.length === 0) {
|
||||
$ths = $('#' + sTableId + ' thead th');
|
||||
}
|
||||
|
||||
var oSettings = oDt.settings()[0];
|
||||
$ths.each(function (iIdx) {
|
||||
var oCol = oSettings.aoColumns[iIdx];
|
||||
if (oCol && typeof oCol.mData === 'string') {
|
||||
$(this).addClass('ibo-datatable--tree-sortable');
|
||||
}
|
||||
});
|
||||
|
||||
$ths.off('click.itop-external-key-grouping').on('click.itop-external-key-grouping', function () {
|
||||
var iColIndex = $(this).index();
|
||||
var oCol = oSettings.aoColumns[iColIndex];
|
||||
if (!oCol || typeof oCol.mData !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (oState.sortColIndex === iColIndex) {
|
||||
oState.sortDir = oState.sortDir === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
oState.sortColIndex = iColIndex;
|
||||
oState.sortDir = 'asc';
|
||||
}
|
||||
|
||||
// Update visual indicators
|
||||
$ths.removeClass('ibo-datatable--tree-sort-asc ibo-datatable--tree-sort-desc');
|
||||
$(this).addClass('ibo-datatable--tree-sort-' + oState.sortDir);
|
||||
|
||||
// Re-fetch from server with new sort params, preserving collapse state
|
||||
_fetchData(false);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Event: collapse / expand a node ───────────────────────────────────────
|
||||
$('#' + sTableId).on('click', '.ibo-datatable--tree-toggle', function (e) {
|
||||
e.stopPropagation();
|
||||
var sNodeId = $(this).attr('data-node-id');
|
||||
if (oState.collapsed.has(sNodeId)) {
|
||||
oState.collapsed.delete(sNodeId);
|
||||
} else {
|
||||
oState.collapsed.add(sNodeId);
|
||||
}
|
||||
_redraw();
|
||||
});
|
||||
|
||||
// ── Event: apply tree UI after every DataTable draw ───────────────────────
|
||||
$('#' + sTableId).on('draw.dt', function () {
|
||||
_applyTreeUi();
|
||||
});
|
||||
|
||||
// ── Event: refresh (triggered externally via refresh.datatable.itop) ──────
|
||||
$('#' + sTableId).on('refresh.datatable.itop', function () {
|
||||
_fetchData(true);
|
||||
});
|
||||
|
||||
// ── Bootstrap with pre-loaded initData (no extra round-trip needed) ───────
|
||||
if (oConfig.initData && Array.isArray(oConfig.initData.data)) {
|
||||
oState.allData = oConfig.initData.data;
|
||||
}
|
||||
|
||||
_rebuild();
|
||||
_redraw();
|
||||
_attachSortHandlers();
|
||||
}
|
||||
|
||||
return {setup: setup};
|
||||
|
||||
}(jQuery));
|
||||
@@ -41,6 +41,7 @@ class DataTable extends UIContentBlock
|
||||
'js/dataTables.settings.js',
|
||||
'js/dataTables.pipeline.js',
|
||||
'js/dataTables.row-actions.js',
|
||||
'js/dataTables.tree-grouping.js',
|
||||
];
|
||||
|
||||
protected $aOptions;//list of specific options for display datatable
|
||||
|
||||
@@ -300,6 +300,9 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
|
||||
|
||||
// Initialize and check the parameters
|
||||
$bViewLink = isset($aExtraParams['view_link']) ? $aExtraParams['view_link'] : true;
|
||||
// Check if this is a tree grouping view
|
||||
$sTreeGroupingAttr = $aExtraParams['tree_grouping_attr'] ?? '';
|
||||
$bIsTreeGroupingView = utils::IsNotNullOrEmptyString($sTreeGroupingAttr);
|
||||
// Check if there is a list of aliases to limit the display to...
|
||||
$aDisplayAliases = isset($aExtraParams['display_aliases']) ? explode(',', $aExtraParams['display_aliases']) : [];
|
||||
$sZListName = isset($aExtraParams['zlist']) ? ($aExtraParams['zlist']) : 'list';
|
||||
@@ -452,6 +455,11 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
|
||||
$oCustomSettings = $oDefaultSettings;
|
||||
}
|
||||
|
||||
// Tree grouping mode: Disable pagination so the full dataset is embedded in the page, otherwise there will be glitches in grouping / sorting
|
||||
if ($bIsTreeGroupingView) {
|
||||
$oCustomSettings->iDefaultPageSize = 0;
|
||||
}
|
||||
|
||||
if ($oCustomSettings->iDefaultPageSize > 0) {
|
||||
$oSet->SetLimit($oCustomSettings->iDefaultPageSize);
|
||||
}
|
||||
@@ -467,17 +475,18 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
|
||||
// Load only the requested columns
|
||||
$aColumnsToLoad = [];
|
||||
foreach ($oCustomSettings->aColumns as $sAlias => $aColumnsInfo) {
|
||||
$sClass = $aClassAliases[$sAlias];
|
||||
foreach ($aColumnsInfo as $sAttCode => $aData) {
|
||||
$bForceLoad = false;
|
||||
if ($aData['sort'] != 'none' || isset($oCustomSettings->aSortOrder[$sAttCode])) {
|
||||
$bForceLoad = true;
|
||||
}
|
||||
|
||||
if ($sAttCode != '_key_') {
|
||||
if ($aData['checked'] || $bForceLoad) {
|
||||
$aColumnsToLoad[$sAlias][] = $sAttCode;
|
||||
} else {
|
||||
// See if this column is a must to load
|
||||
$sClass = $aClassAliases[$sAlias];
|
||||
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
|
||||
if ($oAttDef->AlwaysLoadInTables()) {
|
||||
$aColumnsToLoad[$sAlias][] = $sAttCode;
|
||||
@@ -489,6 +498,13 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tree grouping mode: ensure the grouping attribute is loaded even if not displayed
|
||||
if ($bIsTreeGroupingView
|
||||
&& MetaModel::IsValidAttCode($sClass, $sTreeGroupingAttr)
|
||||
&& !in_array($sTreeGroupingAttr, $aColumnsToLoad[$sAlias] ?? [])) {
|
||||
$aColumnsToLoad[$sAlias][] = $sTreeGroupingAttr;
|
||||
}
|
||||
}
|
||||
$oSet->OptimizeColumnLoad($aColumnsToLoad);
|
||||
|
||||
@@ -615,6 +631,14 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
|
||||
$aExtraParams['table_id'] = $sTableId;
|
||||
$aExtraParams['list_id'] = $sListId;
|
||||
|
||||
// Tree grouping options
|
||||
if ($bIsTreeGroupingView) {
|
||||
$sFirstAlias = array_key_first($aAuthorizedClasses);
|
||||
$aOptions['sTreeGroupingAttr'] = $sTreeGroupingAttr;
|
||||
// The JS reads the parent ID from this data key in each row
|
||||
$aOptions['sTreeGroupingParentKey'] = $sFirstAlias.'/'.$sTreeGroupingAttr.'/raw';
|
||||
}
|
||||
|
||||
$oDataTable->SetOptions($aOptions);
|
||||
$oDataTable->SetAjaxUrl(utils::GetAbsoluteUrlAppRoot()."pages/ajax.render.php");
|
||||
$oDataTable->SetAjaxData([
|
||||
@@ -984,6 +1008,8 @@ JS;
|
||||
/** Don't provide the standard object creation feature */
|
||||
'display_unauthorized_objects',
|
||||
/** bool Display objects for which the user has no read rights */
|
||||
'tree_grouping_attr',
|
||||
/** string Attribute code to group objects on as a collapsible tree */
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,10 +115,10 @@ class AjaxRenderController
|
||||
|
||||
foreach ($aColumnsLoad[$sAlias] as $sAttCode) {
|
||||
$aObj[$sAlias."/".$sAttCode] = $aObject[$sAlias]->GetAsHTML($sAttCode);
|
||||
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
|
||||
$bExcludeRawValue = false;
|
||||
// Only retrieve raw (stored) value for simple fields
|
||||
foreach (cmdbAbstractObject::GetAttDefClassesToExcludeFromMarkupMetadataRawValue() as $sAttDefClassToExclude) {
|
||||
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
|
||||
if (is_a($oAttDef, $sAttDefClassToExclude, true)) {
|
||||
$bExcludeRawValue = true;
|
||||
break;
|
||||
@@ -500,6 +500,14 @@ class AjaxRenderController
|
||||
foreach (MetaModel::GetAttributesToAlwaysLoadInTables($sClassName) as $sAttCode) {
|
||||
$aColumnsLoad[$sAlias][] = $sAttCode;
|
||||
}
|
||||
|
||||
// Tree grouping: ensure the parent key attribute is loaded so the frontend can build parent-child links
|
||||
$sTreeGroupingAttr = $extraParams['tree_grouping_attr'] ?? '';
|
||||
if (utils::IsNotNullOrEmptyString($sTreeGroupingAttr)
|
||||
&& MetaModel::IsValidAttCode($sClassName, $sTreeGroupingAttr)
|
||||
&& !in_array($sTreeGroupingAttr, $aColumnsLoad[$sAlias])) {
|
||||
$aColumnsLoad[$sAlias][] = $sTreeGroupingAttr;
|
||||
}
|
||||
}
|
||||
$aQueryParams = isset($aExtraParams['query_params']) ? $aExtraParams['query_params'] : [];
|
||||
|
||||
|
||||
@@ -18,6 +18,107 @@ if ($('#{{ oUIBlock.GetId() }}') != 'undefined' && $.fn.dataTable.isDataTable('#
|
||||
$('#{{ oUIBlock.GetId() }}').DataTable().destroy(false);
|
||||
}
|
||||
|
||||
{% if oUIBlock.GetOption('sTreeGroupingAttr') is not empty %}
|
||||
{# ── HIERARCHICAL TREE MODE ─────────────────────────────────────────────────── #}
|
||||
var oTable{{ sListIDForVarSuffix }} = $('#{{ oUIBlock.GetId() }}').DataTable({
|
||||
language: {
|
||||
processing: "{{ 'UI:Datatables:Language:Processing'|dict_s }}",
|
||||
search: "{{ 'UI:Datatables:Language:Search'|dict_s }}",
|
||||
lengthMenu: "{{ 'UI:Datatables:Language:LengthMenu'|dict_s }}",
|
||||
zeroRecords: "{{ 'UI:Datatables:Language:ZeroRecords'|dict_s }}",
|
||||
info: "{{ 'UI:Datatables:Language:Info'|dict_s }}",
|
||||
infoEmpty: "{{ 'UI:Datatables:Language:InfoEmpty'|dict_s }}",
|
||||
infoFiltered: "({{ 'UI:Datatables:Language:InfoFiltered'|dict_s }})",
|
||||
emptyTable: "{{ 'UI:Datatables:Language:EmptyTable'|dict_s }}",
|
||||
paginate: {
|
||||
first: "<i class=\"fas fa-angle-double-left\"></i>",
|
||||
previous: "<i class=\"fas fa-angle-left\"></i>",
|
||||
next: "<i class=\"fas fa-angle-right\"></i>",
|
||||
last: "<i class=\"fas fa-angle-double-right\"></i>"
|
||||
}
|
||||
},
|
||||
scrollX: true,
|
||||
{% if oUIBlock.GetOption('sMaxHeight') is not empty %}
|
||||
scrollY: "{{ oUIBlock.GetOption('sMaxHeight') }}",
|
||||
{% endif %}
|
||||
scrollCollapse: true,
|
||||
paging: false,
|
||||
ordering: false,
|
||||
dom: "<'ibo-datatable--toolbar'<'ibo-datatable--toolbar-left'><'ibo-datatable--toolbar-right' i>>t",
|
||||
rowId: "id",
|
||||
filter: false,
|
||||
retrieve: true,
|
||||
destroy: true,
|
||||
processing: false,
|
||||
serverSide: false,
|
||||
data: [],
|
||||
columns: [
|
||||
{% for aColumn in oUIBlock.GetDisplayColumns() %}
|
||||
{
|
||||
autoWidth: true,
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
title: "{{ aColumn["attribute_label"] }}",
|
||||
defaultContent: "",
|
||||
type: "html",
|
||||
metadata: {
|
||||
object_class: "{{ aColumn["object_class"] }}",
|
||||
attribute_code: "{{ aColumn["attribute_code"] }}",
|
||||
attribute_type: "{{ aColumn["attribute_type"] }}",
|
||||
attribute_label: "{{ aColumn["attribute_label"] }}"
|
||||
},
|
||||
data: "{{ aColumn["class_alias"] }}/{{ aColumn["attribute_code"] }}",
|
||||
createdCell: function (td, cellData, rowData, row, col) {
|
||||
$(td).attr('data-object-class', '{{ aColumn["object_class"] }}');
|
||||
$(td).attr('data-attribute-label', '{{ aColumn["attribute_label"] }}');
|
||||
{% if aColumn["attribute_code"] != "_key_" %}
|
||||
$(td).attr('data-attribute-code', '{{ aColumn["attribute_code"] }}');
|
||||
$(td).attr('data-attribute-type', '{{ aColumn["attribute_type"] }}');
|
||||
{% endif %}
|
||||
if (rowData["{{ aColumn["class_alias"] }}/{{ aColumn["attribute_code"] }}/raw"]) {
|
||||
$(td).attr('data-value-raw', rowData["{{ aColumn["class_alias"] }}/{{ aColumn["attribute_code"] }}/raw"]);
|
||||
}
|
||||
},
|
||||
render: {
|
||||
display: function (data, type, row) { {{ aColumn["render"]|raw }}},
|
||||
_: "{{ aColumn["class_alias"] }}/{{ aColumn["attribute_code"] }}"
|
||||
}
|
||||
},
|
||||
{% endfor %}
|
||||
],
|
||||
drawCallback: function (settings) {
|
||||
$(this).closest('.dataTables_wrapper').unblock();
|
||||
},
|
||||
createdRow: function (row, data, dataIndex) {
|
||||
if (data['@class'] !== undefined) {
|
||||
$(row).addClass(data['@class']);
|
||||
}
|
||||
},
|
||||
initComplete: function () {
|
||||
this.attr('data-status', 'loaded');
|
||||
this.closest('.dataTables_scroll').find('.dataTables_scrollHead .ibo-datatable').attr('data-status', 'loaded');
|
||||
|
||||
iTopDataTableTreeGrouping.setup('{{ oUIBlock.GetId() }}', this.api(), {
|
||||
parentKey: '{{ oUIBlock.GetOption("sTreeGroupingParentKey") }}',
|
||||
ajaxUrl: '{{ oUIBlock.GetAjaxUrl() }}',
|
||||
ajaxData: {{ oUIBlock.GetJsonAjaxData() | raw }},
|
||||
initData: {{ oUIBlock.GetJsonInitDisplayData() | raw }}
|
||||
});
|
||||
|
||||
if (window.ResizeObserver) {
|
||||
let oTable{{ sListIDForVarSuffix }}ResizeTimeout = null;
|
||||
const oTable{{ sListIDForVarSuffix }}Resize = new ResizeObserver(function () {
|
||||
clearTimeout(oTable{{ sListIDForVarSuffix }}ResizeTimeout);
|
||||
oTable{{ sListIDForVarSuffix }}ResizeTimeout = setTimeout(function () {
|
||||
$('#{{ oUIBlock.GetId() }}').DataTable().columns.adjust();
|
||||
}, 120);
|
||||
});
|
||||
oTable{{ sListIDForVarSuffix }}Resize.observe($('#{{ oUIBlock.GetId() }}')[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
{% else %}
|
||||
{# ── NORMAL SERVER-SIDE MODE ────────────────────────────────────────────────── #}
|
||||
var oTable{{ sListIDForVarSuffix }} = $('#{{ oUIBlock.GetId() }}').DataTable({
|
||||
language: {
|
||||
processing: "{{ 'UI:Datatables:Language:Processing'|dict_s }}",
|
||||
@@ -359,7 +460,10 @@ var oTable{{ sListIDForVarSuffix }} = $('#{{ oUIBlock.GetId() }}').DataTable({
|
||||
this.closest('.dataTables_scroll').find('.dataTables_scrollHead .ibo-datatable').attr('data-status', 'loaded');
|
||||
}
|
||||
});
|
||||
{% endif %}{# end {% else %} normal mode #}
|
||||
|
||||
{% if oUIBlock.GetOption('sTreeGroupingAttr') is empty %}
|
||||
{# ── POST-INIT: normal-mode-only behaviors ───────────────────────────────── #}
|
||||
oTable{{ sListIDForVarSuffix }}.select();
|
||||
|
||||
$('#{{ oUIBlock.GetId() }}').on('refresh.datatable.itop', function (){
|
||||
@@ -379,7 +483,9 @@ $('#{{ sListId }} [name="selectionCount"]').on('change', function () {
|
||||
$('{{ oUIBlock.GetOption('sCountSelector') }}').trigger('change');
|
||||
});
|
||||
{% endif %}
|
||||
{% endif %}{# end normal-mode-only post-init #}
|
||||
|
||||
{# ── CONFIGURE THIS LIST DIALOG (both modes) ──────────────────────────────── #}
|
||||
$('#datatable_dlg_{{ oUIBlock.GetId() }}').dialog(
|
||||
{
|
||||
autoOpen: false,
|
||||
@@ -389,8 +495,9 @@ $('#datatable_dlg_{{ oUIBlock.GetId() }}').dialog(
|
||||
open: function () {
|
||||
$('#datatable_dlg_{{ oUIBlock.GetId() }}').find('[name=action]').val("none");
|
||||
},
|
||||
close: function (event, ui) { //save data and refresh
|
||||
if ($('#datatable_dlg_{{ oUIBlock.GetId() }}').find('[name=action]').val() === "none")
|
||||
close: function (event, ui) {
|
||||
var sAction = $('#datatable_dlg_{{ oUIBlock.GetId() }}').find('[name=action]').val();
|
||||
if (sAction === "none")
|
||||
{
|
||||
$('#datatable_dlg_{{ oUIBlock.GetId() }}').DataTableSettings('onDlgCancel');
|
||||
}
|
||||
@@ -435,6 +542,86 @@ if ($('#datatable_dlg_{{ oUIBlock.GetId() }}').hasClass('itop-datatable'))
|
||||
}
|
||||
$('#datatable_dlg_{{ oUIBlock.GetId() }}').DataTableSettings(aOptions{{ sListIDForVarSuffix }});
|
||||
|
||||
{% if oUIBlock.GetOption('sTreeGroupingAttr') is not empty %}
|
||||
{# Tree mode: replace DataTableSettings._refresh() so "Configure this list" rebuilds
|
||||
the DataTable client-side in tree mode, instead of switching it to server-side. #}
|
||||
var sTreeParentKey{{ sListIDForVarSuffix }} = '{{ oUIBlock.GetOption("sTreeGroupingParentKey") }}';
|
||||
var oTreePlugin{{ sListIDForVarSuffix }} = $('#datatable_dlg_{{ oUIBlock.GetId() }}').data('itop-DataTableSettings');
|
||||
if (oTreePlugin{{ sListIDForVarSuffix }}) {
|
||||
oTreePlugin{{ sListIDForVarSuffix }}._refresh = function() {
|
||||
var me = this;
|
||||
var oParams = me.options.oData;
|
||||
oParams.operation = 'search_and_refresh';
|
||||
oParams.start = 0;
|
||||
oParams.end = me.options.iPageSize;
|
||||
oParams.select_mode = me.options.sSelectMode;
|
||||
oParams.display_key = me.options.sViewLink;
|
||||
oParams.class_aliases = me.options.oClassAliases;
|
||||
oParams.columns = me.options.oColumns;
|
||||
oParams.list_id = me.options.sListId;
|
||||
me.element.block();
|
||||
$.post(me.options.sRenderUrl, oParams, function(data) {
|
||||
var $table = $('#' + me.options.sListId);
|
||||
var $parent = $table.closest('.dataTables_wrapper').parent();
|
||||
var aPrevOptions = $table.DataTable().context[0].oInit;
|
||||
$table.DataTable().destroy(true);
|
||||
|
||||
var aOptions = $.extend({}, aPrevOptions, JSON.parse(data));
|
||||
|
||||
// Compile per-column render functions returned as strings by the server
|
||||
$.each(aOptions['columns'], function (i) {
|
||||
aOptions['columns'][i]['render']['display'] = new Function("data, type, row", aOptions['columns'][i]['render']['display']);
|
||||
if (aOptions['columns'][i]['createdCell'] != undefined) {
|
||||
aOptions['columns'][i]['createdCell'] = new Function("td, cellData, rowData, row, col", aOptions['columns'][i]['createdCell']);
|
||||
}
|
||||
// Tree mode handles its own sorting — disable native ordering per column
|
||||
aOptions['columns'][i].orderable = false;
|
||||
aOptions['columns'][i].sortable = false;
|
||||
});
|
||||
|
||||
// Rebuild <thead> from the new column list
|
||||
var sThead = '';
|
||||
$.each(aOptions['allColumns'], function (i, item) {
|
||||
$.each(item, function (j, champs) {
|
||||
if (champs.checked == 'true') {
|
||||
sThead += '<th>' + champs.label + '</th>';
|
||||
}
|
||||
});
|
||||
});
|
||||
$parent.append('<table id="' + me.options.sListId + '" width="100%" class="ibo-datatable"><thead><tr>' + sThead + '</tr></thead></table>');
|
||||
|
||||
// Force tree-mode options (the server response is shaped for server-side mode)
|
||||
aOptions.serverSide = false;
|
||||
aOptions.paging = false;
|
||||
aOptions.ordering = false;
|
||||
aOptions.data = [];
|
||||
aOptions.dom = "<'ibo-datatable--toolbar'<'ibo-datatable--toolbar-left'><'ibo-datatable--toolbar-right' i>>t";
|
||||
delete aOptions.ajax;
|
||||
|
||||
// ajaxData passed to tree-grouping setup: same params, but operation=search to fetch rows
|
||||
var oTreeAjaxData = $.extend({}, oParams);
|
||||
oTreeAjaxData.operation = 'search';
|
||||
|
||||
aOptions.initComplete = function () {
|
||||
this.attr('data-status', 'loaded');
|
||||
this.closest('.dataTables_scroll').find('.dataTables_scrollHead .ibo-datatable').attr('data-status', 'loaded');
|
||||
iTopDataTableTreeGrouping.setup(me.options.sListId, this.api(), {
|
||||
parentKey: sTreeParentKey{{ sListIDForVarSuffix }},
|
||||
ajaxUrl: me.options.sRenderUrl,
|
||||
ajaxData: oTreeAjaxData,
|
||||
initData: null
|
||||
});
|
||||
// Fetch rows and rebuild the tree
|
||||
$('#' + me.options.sListId).trigger('refresh.datatable.itop');
|
||||
};
|
||||
|
||||
$('#' + me.options.sListId).DataTable(aOptions);
|
||||
me.element.unblock();
|
||||
}, 'html');
|
||||
};
|
||||
}
|
||||
{% else %}
|
||||
{# Normal mode: ResizeObserver and row actions #}
|
||||
if(window.ResizeObserver){
|
||||
let oTable{{ sListIDForVarSuffix }}ResizeTimeout = null;
|
||||
const oTable{{ sListIDForVarSuffix }}Resize = new ResizeObserver(function(){
|
||||
@@ -448,4 +635,5 @@ if(window.ResizeObserver){
|
||||
|
||||
{% if oUIBlock.HasRowActions() %}
|
||||
{% include 'base/components/datatable/row-actions/handler.js.twig' %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}{# end mode-specific post-init #}
|
||||
Reference in New Issue
Block a user