mirror of
https://github.com/Combodo/iTop.git
synced 2026-04-21 01:28:47 +02:00
N°3531 - Activity panel: Restore possibility to load extra history entries asynchroniously
This commit is contained in:
@@ -58,6 +58,11 @@ $ibo-activity-entry--sub-information--text-color: $ibo-color-grey-700 !default;
|
||||
|
||||
$ibo-activity-entry--author-name--sibling-spacing: 0.2rem !default;
|
||||
|
||||
$ibo-activity-panel--load-more-entries--size: 32px !default;
|
||||
$ibo-activity-panel--load-more-entries--border-radius: $ibo-border-radius-full !default;
|
||||
$ibo-activity-panel--load-more-entries--background-color: $ibo-activity-entry--main-information--background-color !default;
|
||||
$ibo-activity-panel--load-more-entries--border: $ibo-content-block--border !default;
|
||||
|
||||
/* Entry group */
|
||||
.ibo-activity-panel--entry-group{
|
||||
&:not(:last-child){
|
||||
@@ -228,4 +233,18 @@ $ibo-activity-entry--author-name--sibling-spacing: 0.2rem !default;
|
||||
margin-left: $ibo-activity-entry--author-name--sibling-spacing;
|
||||
margin-right: $ibo-activity-entry--author-name--sibling-spacing;
|
||||
}
|
||||
}
|
||||
|
||||
.ibo-activity-panel--load-more-entries-container {
|
||||
@extend %ibo-fully-centered-content;
|
||||
}
|
||||
|
||||
.ibo-activity-panel--load-more-entries {
|
||||
width: $ibo-activity-panel--load-more-entries--size;
|
||||
height: $ibo-activity-panel--load-more-entries--size;
|
||||
border-radius: $ibo-activity-panel--load-more-entries--border-radius;
|
||||
background-color: $ibo-activity-panel--load-more-entries--background-color;
|
||||
border: $ibo-activity-panel--load-more-entries--border;
|
||||
@extend %ibo-fully-centered-content;
|
||||
@extend %ibo-hyperlink-inherited-colors;
|
||||
}
|
||||
@@ -22,6 +22,7 @@ Dict::Add('EN US', 'English', 'English', array(
|
||||
'UI:Layout:ActivityPanel:SizeToggler:Expand:Tooltip' => 'Expand',
|
||||
'UI:Layout:ActivityPanel:SizeToggler:Reduce:Tooltip' => 'Reduce',
|
||||
'UI:Layout:ActivityPanel:DisplayToggler:Close:Tooltip' => 'Close',
|
||||
'UI:Layout:ActivityPanel:LoadMoreEntries:Tooltip' => 'Load previous entries',
|
||||
|
||||
// Tabs
|
||||
'UI:Layout:ActivityPanel:Tab:Activity:Title' => 'Activity',
|
||||
|
||||
@@ -34,6 +34,8 @@ $(function()
|
||||
lock_endpoint: null,
|
||||
show_multiple_entries_submit_confirmation: true,
|
||||
save_state_endpoint: null,
|
||||
last_loaded_entries_ids: {},
|
||||
load_more_entries_endpoint: null,
|
||||
},
|
||||
css_classes:
|
||||
{
|
||||
@@ -84,6 +86,9 @@ $(function()
|
||||
edits_entry_long_description_toggler: '[data-role="ibo-edits-entry--long-description-toggler"]',
|
||||
notification_entry_long_description: '[data-role="ibo-notification-entry--long-description"]',
|
||||
notification_entry_long_description_toggler: '[data-role="ibo-notification-entry--long-description-toggler"]',
|
||||
load_more_entries_container: '[data-role="ibo-activity-panel--load-more-entries-container"]',
|
||||
load_more_entries: '[data-role="ibo-activity-panel--load-more-entries"]',
|
||||
load_more_entries_icon: '[data-role="ibo-activity-panel--load-more-entries-icon"]',
|
||||
},
|
||||
enums: {
|
||||
tab_types: {
|
||||
@@ -203,17 +208,21 @@ $(function()
|
||||
|
||||
// Entries
|
||||
// - Click on a closed case log message
|
||||
this.element.find(this.js_selectors.entry_group).on('click', '.'+this.css_classes.is_closed+' '+this.js_selectors.entry_main_information, function (oEvent) {
|
||||
this.element.on('click', this.js_selectors.entry+'.'+this.css_classes.is_closed+' '+this.js_selectors.entry_main_information, function (oEvent) {
|
||||
me._onCaseLogClosedMessageClick($(this).closest(me.js_selectors.entry));
|
||||
});
|
||||
// - Click on an edits entry's long description toggler
|
||||
this.element.find(this.js_selectors.edits_entry_long_description_toggler).on('click', function (oEvent) {
|
||||
this.element.on('click', this.js_selectors.edits_entry_long_description_toggler, function (oEvent) {
|
||||
me._onEntryLongDescriptionTogglerClick(oEvent, $(this).closest(me.js_selectors.entry));
|
||||
});
|
||||
// - Click on an notification entry's long description toggler
|
||||
this.element.find(this.js_selectors.notification_entry_long_description_toggler).on('click', function (oEvent) {
|
||||
this.element.on('click', this.js_selectors.notification_entry_long_description_toggler, function (oEvent) {
|
||||
me._onEntryLongDescriptionTogglerClick(oEvent, $(this).closest(me.js_selectors.entry));
|
||||
});
|
||||
// - Click on load more entries button
|
||||
this.element.find(this.js_selectors.load_more_entries).on('click', function (oEvent) {
|
||||
me._onLoadMoreEntriesButtonClick(oEvent);
|
||||
});
|
||||
|
||||
// Page exit
|
||||
// - Show confirm dialog if draft entries (IMPORTANT: Lock is NOT released, see N°3786)
|
||||
@@ -345,6 +354,16 @@ $(function()
|
||||
// Else, open a popover menu to choose one
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param oEvent {Object}
|
||||
* @return {void}
|
||||
* @private
|
||||
*/
|
||||
_onLoadMoreEntriesButtonClick: function (oEvent) {
|
||||
oEvent.preventDefault();
|
||||
|
||||
this._LoadMoreEntries();
|
||||
},
|
||||
/**
|
||||
* Indicate that there is a draft entry and will request lock on the object
|
||||
*
|
||||
@@ -820,7 +839,7 @@ $(function()
|
||||
|
||||
// Update the feed
|
||||
for (let sCaseLogAttCode in oData.data.entries) {
|
||||
me._AddEntry(sCaseLogAttCode, oData.data.entries[sCaseLogAttCode]);
|
||||
me._AddEntry(oData.data.entries[sCaseLogAttCode], 'start');
|
||||
}
|
||||
me._ApplyEntriesFilters();
|
||||
|
||||
@@ -1168,20 +1187,88 @@ $(function()
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Add an entry represented by its oData to the case log with the sCaseLogAttCode
|
||||
* Load the next entries and append them to the current ones
|
||||
*
|
||||
* IMPORTANT: For now the logic is naive, the entries come from 3 different sources : case logs, CMDB change ops and notifications.
|
||||
* We load all the case logs and notifications entries, but only the 'max_history_length' first from the CMDB change ops.
|
||||
*
|
||||
* When we load the remaining history entries (CMDB change ops) and append them to the activity panel, some of them should actually
|
||||
* be placed between already present entries (case logs, notifications) to keep the chronological order. This is a known limitation
|
||||
* and might be worked on in a future version.
|
||||
*
|
||||
* @private
|
||||
* @return {void}
|
||||
*/
|
||||
_LoadMoreEntries: function () {
|
||||
const me = this;
|
||||
|
||||
// Change icon to spinning
|
||||
this.element.find(this.js_selectors.load_more_entries_icon)
|
||||
.removeClass('fas fa-angle-double-down')
|
||||
.addClass('fas fa-sync-alt fa-spin');
|
||||
|
||||
// Send XHR request
|
||||
let oParams = {
|
||||
operation: 'activity_panel_load_more_entries',
|
||||
object_class: this._GetHostObjectClass(),
|
||||
object_id: this._GetHostObjectID(),
|
||||
last_loaded_entries_ids: this.options.last_loaded_entries_ids,
|
||||
};
|
||||
$.post(
|
||||
this.options.load_more_entries_endpoint,
|
||||
oParams,
|
||||
'json'
|
||||
)
|
||||
.fail(function (oXHR, sStatus, sErroThrown) {
|
||||
// TODO 3.0.0: Maybe we could have a centralized dialog to display error messages?
|
||||
alert(sErrorThrown);
|
||||
})
|
||||
.done(function (oData) {
|
||||
if (false === oData.data.success) {
|
||||
// TODO 3.0.0: Same comment as the fail() callback
|
||||
alert(oData.data.error_message);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update the feed
|
||||
for (let oEntry of oData.data.entries) {
|
||||
me._AddEntry(oEntry, 'end');
|
||||
}
|
||||
me._ApplyEntriesFilters();
|
||||
|
||||
// Check if more entries to load
|
||||
// - Update metadata
|
||||
me.options.last_loaded_entries_ids = oData.data.last_loaded_entries_ids;
|
||||
// - Update button state
|
||||
if (Object.keys(me.options.last_loaded_entries_ids).length === 0) {
|
||||
me.element.find(me.js_selectors.load_more_entries).addClass(me.css_classes.is_hidden);
|
||||
}
|
||||
})
|
||||
.always(function () {
|
||||
// Change icon back to original (whether it should be displayed or not will be handle by thes other callbacks)
|
||||
// - fail => keep displayed for retry
|
||||
// - done => display only if more entries to load
|
||||
me.element.find(me.js_selectors.load_more_entries_icon)
|
||||
.removeClass('fas fa-sync-alt fa-spin')
|
||||
.addClass('fas fa-angle-double-down');
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Add an entry represented by its oData to the feed
|
||||
*
|
||||
* @param sCaseLogAttCode {string}
|
||||
* @param oData {Object} Structured data of the entry: {html_rendering: <HTML_DATA>}
|
||||
* @param sPosition {string} Whether the entry should be added at the 'start' or 'end' of the feed
|
||||
* @private
|
||||
*/
|
||||
_AddEntry: function (sCaseLogAttCode, oData) {
|
||||
_AddEntry: function (oData, sPosition = 'start') {
|
||||
// Info about the new entry
|
||||
const oNewEntryElem = $(oData.html_rendering);
|
||||
const sNewEntryAuthorLogin = oNewEntryElem.attr('data-entry-author-login');
|
||||
const sNewEntryOrigin = oNewEntryElem.attr('data-entry-group-origin');
|
||||
|
||||
// Info about the last entry group to see the entry to add should be in this one or a new one
|
||||
const oLastEntryGroupElem = this.element.find(this.js_selectors.entry_group+':first');
|
||||
const sEntryGroupPosition = (sPosition === 'start') ? 'first' : 'last';
|
||||
const oLastEntryGroupElem = this.element.find(this.js_selectors.entry_group+':'+sEntryGroupPosition);
|
||||
const sLastEntryAuthorLogin = oLastEntryGroupElem.length > 0 ? oLastEntryGroupElem.attr('data-entry-author-login') : null;
|
||||
const sLastEntryOrigin = oLastEntryGroupElem.length > 0 ? oLastEntryGroupElem.attr('data-entry-group-origin') : null;
|
||||
|
||||
@@ -1189,9 +1276,12 @@ $(function()
|
||||
if ((sLastEntryAuthorLogin === sNewEntryAuthorLogin) && (sLastEntryOrigin && sNewEntryOrigin)) {
|
||||
oTargetEntryGroup = oLastEntryGroupElem;
|
||||
} else {
|
||||
oTargetEntryGroup = this._CreateEntryGroup(sNewEntryAuthorLogin, sNewEntryOrigin);
|
||||
oTargetEntryGroup = this._CreateEntryGroup(sNewEntryAuthorLogin, sNewEntryOrigin, sPosition);
|
||||
}
|
||||
|
||||
const sInsertFunction = (sPosition === 'start') ? 'prepend' : 'append';
|
||||
oTargetEntryGroup.prepend(oNewEntryElem);
|
||||
|
||||
this._ReformatDateTimes();
|
||||
},
|
||||
/**
|
||||
@@ -1199,20 +1289,26 @@ $(function()
|
||||
*
|
||||
* @param sAuthorLogin {string}
|
||||
* @param sOrigin {string}
|
||||
* @param sPosition {string} Whether the entry group should be added at the start or the end of the feed
|
||||
* @returns {Object} jQuery object representing the created entry group
|
||||
* @private
|
||||
*/
|
||||
_CreateEntryGroup: function (sAuthorLogin, sOrigin) {
|
||||
_CreateEntryGroup: function (sAuthorLogin, sOrigin, sPosition = 'start') {
|
||||
// Note: When using the ActivityPanel, there should always be at least one entry group already, the one from the object creation
|
||||
let oEntryGroupElem = this.element.find(this.js_selectors.entry_group+':first')
|
||||
.clone()
|
||||
.attr('data-entry-author-login', sAuthorLogin)
|
||||
.attr('data-entry-group-origin', sOrigin)
|
||||
.addClass(this.css_classes.is_current_user)
|
||||
.html('')
|
||||
.prependTo(this.element.find(this.js_selectors.body));
|
||||
.html('');
|
||||
|
||||
if ('start' === sPosition) {
|
||||
oEntryGroupElem.prependTo(this.element.find(this.js_selectors.body));
|
||||
} else {
|
||||
oEntryGroupElem.insertBefore(this.element.find(this.js_selectors.load_more_entries_container));
|
||||
}
|
||||
|
||||
return oEntryGroupElem;
|
||||
},
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2767,6 +2767,24 @@ EOF
|
||||
$oPage->SetData($aResult);
|
||||
break;
|
||||
|
||||
/** @internal */
|
||||
case 'activity_panel_load_more_entries':
|
||||
$oPage = new JsonPage();
|
||||
try {
|
||||
$aResult = ActivityPanelController::LoadMoreEntries();
|
||||
}
|
||||
catch (Exception $oException) {
|
||||
$aResult = [
|
||||
'success' => false,
|
||||
'error_message' => $oException->getMessage(),
|
||||
];
|
||||
}
|
||||
$oPage->SetData($aResult);
|
||||
break;
|
||||
|
||||
//--------------------------------
|
||||
// Navigation menu
|
||||
//--------------------------------
|
||||
case 'get_menus_count':
|
||||
|
||||
$oAjaxRenderController->GetMenusCount($oPage);
|
||||
|
||||
@@ -46,10 +46,10 @@ class ActivityPanelController
|
||||
* 'success' => true,
|
||||
* 'entries' => [
|
||||
* '<ATT_CODE_1>' => [
|
||||
* html_rendering => '<HTML_RENDERING_TO_BE_APPEND_IN_FRONT_END>',
|
||||
* 'html_rendering' => '<HTML_RENDERING_TO_BE_APPEND_IN_FRONT_END>',
|
||||
* ],
|
||||
* '<ATT_CODE_2>' => [
|
||||
* html_rendering => '<HTML_RENDERING_TO_BE_APPEND_IN_FRONT_END>',
|
||||
* 'html_rendering' => '<HTML_RENDERING_TO_BE_APPEND_IN_FRONT_END>',
|
||||
* ],
|
||||
* ...
|
||||
* ],
|
||||
@@ -115,4 +115,59 @@ class ActivityPanelController
|
||||
|
||||
return $aResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load next entries chunk
|
||||
*
|
||||
* @return array Entries already rendered and metadata for pagination
|
||||
* [
|
||||
* 'success' => true,
|
||||
* 'entries' => [
|
||||
* ['html_rendering' => '<HTML_RENDERING>'],
|
||||
* ['html_rendering' => '<HTML_RENDERING>'],
|
||||
* ...
|
||||
* ],
|
||||
* 'last_loaded_entries_ids' => [
|
||||
* 'cmdbchangeop' => <LAST_ENTRY_ID>,
|
||||
* ],
|
||||
* ]
|
||||
*
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreException
|
||||
* @throws \ReflectionException
|
||||
* @throws \Twig\Error\LoaderError
|
||||
* @throws \Twig\Error\RuntimeError
|
||||
* @throws \Twig\Error\SyntaxError
|
||||
*/
|
||||
public static function LoadMoreEntries(): array
|
||||
{
|
||||
$sObjectClass = utils::ReadPostedParam('object_class', null, utils::ENUM_SANITIZATION_FILTER_CLASS);
|
||||
$sObjectId = utils::ReadPostedParam('object_id', 0);
|
||||
$aLastLoadedEntriesIds = utils::ReadPostedParam('last_loaded_entries_ids', [], utils::ENUM_SANITIZATION_FILTER_RAW_DATA);
|
||||
|
||||
$aResults = [
|
||||
'success' => true,
|
||||
'entries' => [],
|
||||
'last_loaded_entries_ids' => [],
|
||||
];
|
||||
|
||||
// CMDBChangeOp entries
|
||||
if (array_key_exists('cmdbchangeop', $aLastLoadedEntriesIds)) {
|
||||
$aChangesData = ActivityPanelHelper::GetCMDBChangeOpEditsEntriesForObject($sObjectClass, $sObjectId, $aLastLoadedEntriesIds['cmdbchangeop']);
|
||||
|
||||
if (true === $aChangesData['more_entries_to_load']) {
|
||||
$aResults['last_loaded_entries_ids']['cmdbchangeop'] = $aChangesData['last_loaded_entry_id'];
|
||||
}
|
||||
|
||||
/** @var \Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\ActivityEntry\EditsEntry $oEntryBlock */
|
||||
foreach ($aChangesData['entries'] as $oEntryBlock) {
|
||||
$sEntryAsHtml = BlockRenderer::RenderBlockTemplates($oEntryBlock);
|
||||
$aResults['entries'][] = [
|
||||
'html_rendering' => $sEntryAsHtml,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $aResults;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,9 +71,13 @@ class ActivityPanel extends UIBlock
|
||||
protected $aEntries;
|
||||
/** @var bool $bAreEntriesSorted True if the entries have been sorted by date */
|
||||
protected $bAreEntriesSorted;
|
||||
/** @var bool True if there are more entries to load asynchroniously */
|
||||
protected $bHasMoreEntriesToLoad;
|
||||
/** @var array IDs of the last loaded entries of each type, makes it easier to load the next entries asynchronioulsy */
|
||||
protected $aLastLoadedEntriesIds;
|
||||
/**
|
||||
* @var bool True if the host object has states (but not necessary a lifecycle)
|
||||
* @see MetaModel::HasStateAttributeCode()
|
||||
* @var bool True if the host object has states (but not necessary a lifecycle)
|
||||
*/
|
||||
protected $bHasStates;
|
||||
/** @var \Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\CaseLogEntryForm\CaseLogEntryForm[] $aCaseLogTabsEntryForms */
|
||||
@@ -101,6 +105,8 @@ class ActivityPanel extends UIBlock
|
||||
$this->SetObject($oObject);
|
||||
$this->SetEntries($aEntries);
|
||||
$this->bAreEntriesSorted = false;
|
||||
$this->bHasMoreEntriesToLoad = false;
|
||||
$this->aLastLoadedEntriesIds = [];
|
||||
$this->ComputedShowMultipleEntriesSubmitConfirmation();
|
||||
}
|
||||
|
||||
@@ -453,6 +459,52 @@ class ActivityPanel extends UIBlock
|
||||
return !empty($this->aEntries);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see static::$bHasMoreEntriesToLoad
|
||||
*
|
||||
* @param bool $bHasMoreEntriesToLoad
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function SetHasMoreEntriesToLoad(bool $bHasMoreEntriesToLoad)
|
||||
{
|
||||
$this->bHasMoreEntriesToLoad = $bHasMoreEntriesToLoad;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see static::$bHasMoreEntriesToLoad
|
||||
* @return bool
|
||||
*/
|
||||
public function HasMoreEntriesToLoad(): bool
|
||||
{
|
||||
return $this->bHasMoreEntriesToLoad;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sEntryType Type of entry (eg. cmdbchangeop, caselog, notification)
|
||||
* @param string $sEntryId ID of the last loaded entry
|
||||
*
|
||||
* @return $this
|
||||
* @uses static::$aLastLoadedEntriesIds
|
||||
*/
|
||||
public function SetLastEntryId(string $sEntryType, string $sEntryId)
|
||||
{
|
||||
$this->aLastLoadedEntriesIds[$sEntryType] = $sEntryId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array Hash array of the last loaded entries
|
||||
* @uses static::$aLastLoadedEntriesIds
|
||||
*/
|
||||
public function GetLastEntryIds(): array
|
||||
{
|
||||
return $this->aLastLoadedEntriesIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the case log tabs metadata, not their entries
|
||||
*
|
||||
@@ -710,7 +762,7 @@ class ActivityPanel extends UIBlock
|
||||
* @return string The endpoint for all "lock" related operations
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function GetLockEndpointForJSWidget(): string
|
||||
public function GetLockEndpoint(): string
|
||||
{
|
||||
return utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php';
|
||||
}
|
||||
@@ -724,6 +776,15 @@ class ActivityPanel extends UIBlock
|
||||
return utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string The endpoint to load the remaining entries
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function GetLoadMoreEntriesEndpoint(): string
|
||||
{
|
||||
return utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
||||
@@ -21,9 +21,7 @@ namespace Combodo\iTop\Application\UI\Base\Layout\ActivityPanel;
|
||||
|
||||
|
||||
use cmdbAbstractObject;
|
||||
use CMDBChangeOpSetAttributeCaseLog;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\ActivityEntry\ActivityEntryFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\ActivityEntry\EditsEntry;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\CaseLogEntryFormFactory\CaseLogEntryFormFactory;
|
||||
use DBObject;
|
||||
use DBObjectSearch;
|
||||
@@ -61,7 +59,7 @@ class ActivityPanelFactory
|
||||
public static function MakeForObjectDetails(DBObject $oObject, string $sMode = cmdbAbstractObject::DEFAULT_OBJECT_MODE)
|
||||
{
|
||||
$sObjClass = get_class($oObject);
|
||||
$iObjId = $oObject->GetKey();
|
||||
$sObjId = $oObject->GetKey();
|
||||
|
||||
if ($sMode == cmdbAbstractObject::ENUM_OBJECT_MODE_PRINT) {
|
||||
$oActivityPanel = new ActivityPanelPrint($oObject, [], ActivityPanel::BLOCK_CODE);
|
||||
@@ -90,48 +88,19 @@ class ActivityPanelFactory
|
||||
}
|
||||
|
||||
// Retrieve history changes (excluding case logs entries)
|
||||
// - Prepare query to retrieve changes
|
||||
$oChangesSearch = DBObjectSearch::FromOQL('SELECT CMDBChangeOp WHERE objclass = :obj_class AND objkey = :obj_key AND finalclass NOT IN (:excluded_optypes)');
|
||||
// Note: We can't order by date (only) as something multiple CMDBChangeOp rows are inserted at the same time (eg. Delivery model of the "Demo" Organization in the sample data).
|
||||
// As the DB returns rows "chronologically", we get the older first and it messes with the processing. Ordering by the ID is way much simpler and less DB CPU consuming.
|
||||
$oChangesSet = new DBObjectSet($oChangesSearch, ['id' => false], ['obj_class' => $sObjClass, 'obj_key' => $iObjId, 'excluded_optypes' => ['CMDBChangeOpSetAttributeCaseLog']]);
|
||||
$oChangesSet->SetLimit(MetaModel::GetConfig()->Get('max_history_length'));
|
||||
$aChangesData = ActivityPanelHelper::GetCMDBChangeOpEditsEntriesForObject($sObjClass, $sObjId);
|
||||
|
||||
// Prepare previous values to group edits within a same CMDBChange
|
||||
$iPreviousChangeId = 0;
|
||||
$oPreviousEditsEntry = null;
|
||||
|
||||
/** @var \CMDBChangeOp $oChangeOp */
|
||||
while ($oChangeOp = $oChangesSet->Fetch()) {
|
||||
// Skip case log changes as they are handled directly from the attributes themselves (most of them should have been excluded by the OQL above, but some derivated classes could still be retrieved)
|
||||
if ($oChangeOp instanceof CMDBChangeOpSetAttributeCaseLog) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Make entry from CMDBChangeOp
|
||||
$iChangeId = $oChangeOp->Get('change');
|
||||
try {
|
||||
$oEntry = ActivityEntryFactory::MakeFromCmdbChangeOp($oChangeOp);
|
||||
}
|
||||
catch (Exception $oException) {
|
||||
IssueLog::Debug(static::class.': Could not create entry from CMDBChangeOp #'.$oChangeOp->GetKey().' related to '.$oChangeOp->Get('objclass').'::'.$oChangeOp->Get('objkey').': '.$oException->getMessage());
|
||||
continue;
|
||||
}
|
||||
// If same CMDBChange and mergeable edits entry from the same author, we merge them
|
||||
if (($iChangeId == $iPreviousChangeId) && ($oPreviousEditsEntry instanceof EditsEntry) && ($oEntry instanceof EditsEntry) && ($oPreviousEditsEntry->GetAuthorLogin() === $oEntry->GetAuthorLogin())) {
|
||||
$oPreviousEditsEntry->Merge($oEntry);
|
||||
} else {
|
||||
$oActivityPanel->AddEntry($oEntry);
|
||||
|
||||
// Set previous edits entry
|
||||
if ($oEntry instanceof EditsEntry) {
|
||||
$oPreviousEditsEntry = $oEntry;
|
||||
}
|
||||
}
|
||||
|
||||
$iPreviousChangeId = $iChangeId;
|
||||
// - Set metadata for pagination
|
||||
if (true === $aChangesData['more_entries_to_load']) {
|
||||
$oActivityPanel->SetHasMoreEntriesToLoad(true);
|
||||
$oActivityPanel->SetLastEntryId('cmdbchangeop', $aChangesData['last_loaded_entry_id']);
|
||||
}
|
||||
|
||||
// - Add history entries
|
||||
/** @var \Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\ActivityEntry\EditsEntry $oEntry */
|
||||
foreach ($aChangesData['entries'] as $oEntry) {
|
||||
$oActivityPanel->AddEntry($oEntry);
|
||||
}
|
||||
unset($oChangesSet);
|
||||
|
||||
// Retrieving notification events for cmdbAbstractObject only
|
||||
if ($oObject instanceof cmdbAbstractObject) {
|
||||
@@ -141,7 +110,7 @@ class ActivityPanelFactory
|
||||
if (false === empty($aRelatedTriggersIDs)) {
|
||||
// - Prepare query to retrieve events
|
||||
$oNotifEventsSearch = DBObjectSearch::FromOQL('SELECT EN FROM EventNotification AS EN JOIN Action AS A ON EN.action_id = A.id WHERE EN.trigger_id IN (:triggers_ids) AND EN.object_id = :object_id');
|
||||
$oNotifEventsSet = new DBObjectSet($oNotifEventsSearch, ['id' => false], ['triggers_ids' => $aRelatedTriggersIDs, 'object_id' => $iObjId]);
|
||||
$oNotifEventsSet = new DBObjectSet($oNotifEventsSearch, ['id' => false], ['triggers_ids' => $aRelatedTriggersIDs, 'object_id' => $sObjId]);
|
||||
$oNotifEventsSet->SetLimit(MetaModel::GetConfig()->Get('max_history_length'));
|
||||
|
||||
/** @var \EventNotification $oNotifEvent */
|
||||
|
||||
@@ -8,9 +8,17 @@ namespace Combodo\iTop\Application\UI\Base\Layout\ActivityPanel;
|
||||
|
||||
|
||||
use appUserPreferences;
|
||||
use BinaryExpression;
|
||||
use cmdbAbstractObject;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\ActivityEntry\ActivityEntryFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\ActivityEntry\EditsEntry;
|
||||
use DBObjectSearch;
|
||||
use DBObjectSet;
|
||||
use Exception;
|
||||
use FieldExpression;
|
||||
use IssueLog;
|
||||
use MetaModel;
|
||||
use VariableExpression;
|
||||
|
||||
/**
|
||||
* Class ActivityPanelHelper
|
||||
@@ -75,4 +83,99 @@ class ActivityPanelHelper
|
||||
$aStates[$sObjectClass.'::'.$sObjectMode] = $bIsClosed;
|
||||
appUserPreferences::SetPref('activity_panel.is_closed', $aStates);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sObjectClass
|
||||
* @param string $sObjectId
|
||||
* @param string|null $sChangeOpIdToOffsetFrom Entries will be retrieved after this CMDBChangeOp ID. Typically used for pagination.
|
||||
*
|
||||
* @return array The 'max_history_length' edits entries from the CMDBChangeOp of the object, starting from $sChangeOpIdToOffsetFrom. Flag to know if more entries are available and the ID of the last returned entry are also provided.
|
||||
*
|
||||
* [
|
||||
* 'entries' => EditsEntry[],
|
||||
* 'last_loaded_entry_id' => null|int,
|
||||
* 'more_entries_to_load' => bool,
|
||||
* ]
|
||||
*
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreException
|
||||
*/
|
||||
public static function GetCMDBChangeOpEditsEntriesForObject(string $sObjectClass, string $sObjectId, ?string $sChangeOpIdToOffsetFrom = null): array
|
||||
{
|
||||
$iMaxHistoryLength = MetaModel::GetConfig()->Get('max_history_length');
|
||||
$aResults = [
|
||||
'entries' => [],
|
||||
'last_loaded_entry_id' => null,
|
||||
'more_entries_to_load' => false,
|
||||
];
|
||||
|
||||
// - Prepare query to retrieve changes
|
||||
$oSearch = DBObjectSearch::FromOQL('SELECT CO FROM CMDBChangeOp AS CO WHERE CO.objclass = :obj_class AND CO.objkey = :obj_key AND CO.finalclass NOT IN (:excluded_optypes)');
|
||||
$aArgs = ['obj_class' => $sObjectClass, 'obj_key' => $sObjectId, 'excluded_optypes' => ['CMDBChangeOpSetAttributeCaseLog']];
|
||||
|
||||
// - Optional offset condition
|
||||
if (null !== $sChangeOpIdToOffsetFrom) {
|
||||
$oSearch->AddConditionExpression(
|
||||
new BinaryExpression(
|
||||
new FieldExpression('id', 'CO'), '<', new VariableExpression('id')
|
||||
)
|
||||
);
|
||||
$aArgs['id'] = $sChangeOpIdToOffsetFrom;
|
||||
}
|
||||
|
||||
// Note: We can't order by date (only) as something multiple CMDBChangeOp rows are inserted at the same time (eg. Delivery model of the "Demo" Organization in the sample data).
|
||||
// As the DB returns rows "chronologically", we get the older first and it messes with the processing. Ordering by the ID is way much simpler and less DB CPU consuming.
|
||||
$oSet = new DBObjectSet($oSearch, ['id' => false], $aArgs);
|
||||
|
||||
// - Limit history entries to display
|
||||
$bMoreEntriesToLoad = $oSet->CountExceeds($iMaxHistoryLength);
|
||||
$oSet->SetLimit($iMaxHistoryLength);
|
||||
|
||||
// Prepare previous values to group edits within a same CMDBChange
|
||||
$iPreviousChangeId = 0;
|
||||
/** @var string|int $iPreviousChangeOpId Only used for pagination */
|
||||
$iPreviousChangeOpId = 0;
|
||||
$oPreviousEditsEntry = null;
|
||||
|
||||
/** @var \CMDBChangeOp $oChangeOp */
|
||||
while ($oChangeOp = $oSet->Fetch()) {
|
||||
// Skip case log changes as they are handled directly from the attributes themselves (most of them should have been excluded by the OQL above, but some derivated classes could still be retrieved)
|
||||
if ($oChangeOp instanceof CMDBChangeOpSetAttributeCaseLog) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Make entry from CMDBChangeOp
|
||||
$iChangeId = $oChangeOp->Get('change');
|
||||
try {
|
||||
$oEntry = ActivityEntryFactory::MakeFromCmdbChangeOp($oChangeOp);
|
||||
}
|
||||
catch (Exception $oException) {
|
||||
IssueLog::Debug(static::class.': Could not create entry from CMDBChangeOp #'.$oChangeOp->GetKey().' related to '.$oChangeOp->Get('objclass').'::'.$oChangeOp->Get('objkey').': '.$oException->getMessage());
|
||||
continue;
|
||||
}
|
||||
// If same CMDBChange and mergeable edits entry from the same author, we merge them
|
||||
if (($iChangeId == $iPreviousChangeId) && ($oPreviousEditsEntry instanceof EditsEntry) && ($oEntry instanceof EditsEntry) && ($oPreviousEditsEntry->GetAuthorLogin() === $oEntry->GetAuthorLogin())) {
|
||||
$oPreviousEditsEntry->Merge($oEntry);
|
||||
} else {
|
||||
$aResults['entries'][] = $oEntry;
|
||||
|
||||
// Set previous edits entry
|
||||
if ($oEntry instanceof EditsEntry) {
|
||||
$oPreviousEditsEntry = $oEntry;
|
||||
}
|
||||
}
|
||||
|
||||
$iPreviousChangeId = $iChangeId;
|
||||
$iPreviousChangeOpId = $oChangeOp->GetKey();
|
||||
}
|
||||
unset($oSet);
|
||||
|
||||
// - Set last entry ID so the other can be loaded later
|
||||
if ((true === $bMoreEntriesToLoad) && (0 !== $iPreviousChangeOpId)) {
|
||||
$aResults['last_loaded_entry_id'] = $iPreviousChangeOpId;
|
||||
$aResults['more_entries_to_load'] = true;
|
||||
}
|
||||
|
||||
return $aResults;
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,13 @@
|
||||
{% for aEntryGroup in oUIBlock.GetGroupedEntries() %}
|
||||
{{ include('base/layouts/activity-panel/entry-group.html.twig', {aEntryGroup: aEntryGroup}) }}
|
||||
{% endfor %}
|
||||
{% if oUIBlock.HasMoreEntriesToLoad() %}
|
||||
<div class="ibo-activity-panel--load-more-entries-container" data-role="ibo-activity-panel--load-more-entries-container">
|
||||
<a href="#" class="ibo-activity-panel--load-more-entries" data-role="ibo-activity-panel--load-more-entries" data-tooltip-content="{{ 'UI:Layout:ActivityPanel:LoadMoreEntries:Tooltip'|dict_s }}">
|
||||
<span class="ibo-activity-panel--load-more-entries-icon fas fa-fw fa-angle-double-down" data-role="ibo-activity-panel--load-more-entries-icon"></span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="ibo-activity-panel--body--placeholder">
|
||||
<div class="ibo-activity-panel--body--placeholder-image ibo-svg-illustration--container">
|
||||
|
||||
@@ -4,7 +4,9 @@ $('#{{ oUIBlock.GetId() }}').activity_panel({
|
||||
{% if oUIBlock.HasTransactionId() %}transaction_id: {{ oUIBlock.GetTransactionId()|var_export }},{% endif %}
|
||||
lock_enabled: {{ oUIBlock.IsLockEnabled()|var_export }},
|
||||
lock_watcher_period: {{ get_config_parameter('activity_panel.lock_watcher_period') }},
|
||||
lock_endpoint: {{ oUIBlock.GetLockEndpointForJSWidget()|var_export|raw }},
|
||||
lock_endpoint: {{ oUIBlock.GetLockEndpoint()|var_export|raw }},
|
||||
show_multiple_entries_submit_confirmation: {{ oUIBlock.GetShowMultipleEntriesSubmitConfirmation()|var_export }},
|
||||
save_state_endpoint: {{ oUIBlock.GetSaveStateEndpoint()|var_export|raw }}
|
||||
save_state_endpoint: {{ oUIBlock.GetSaveStateEndpoint()|var_export|raw }},
|
||||
last_loaded_entries_ids: {{ oUIBlock.GetLastEntryIds()|json_encode|raw }},
|
||||
load_more_entries_endpoint: {{ oUIBlock.GetLoadMoreEntriesEndpoint()|var_export|raw }},
|
||||
});
|
||||
Reference in New Issue
Block a user