N°3649 - Activity panel: Add concurrent lock mechanism

This commit is contained in:
Molkobain
2021-02-19 11:44:22 +01:00
parent c59301ebc6
commit 4d8930832e
10 changed files with 394 additions and 51 deletions

View File

@@ -1205,6 +1205,14 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'activity_panel.lock_watcher_period' => [
'type' => 'integer',
'description' => 'Period (in second) between lock status update.',
'default' => 30,
'value' => 30,
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'obsolescence.show_obsolete_data' => [
'type' => 'bool',
'description' => 'Default value for the user preference "show obsolete data"',

View File

@@ -4,12 +4,19 @@
*/
$ibo-caselog-entry-form--width: 100% !default;
$ibo-caselog-entry-form--padding-bottom: 12px default;
$ibo-caselog-entry-form--padding-bottom: 12px !default;
$ibo-caselog-entry-form--background-color: $ibo-activity-panel--tab-toolbar--background-color !default;
$ibo-caselog-entry-form--actions--margin-top: 8px !default;
$ibo-caselog-entry-form--actions--margin-bottom: $ibo-caselog-entry-form--actions--margin-top !default;
$ibo-caselog-entry-form--lock-indicator--margin-top: $ibo-caselog-entry-form--padding-bottom !default;
$ibo-caselog-entry-form--lock-icon--size: 32px !default;
$ibo-caselog-entry-form--lock-icon--text-color: $ibo-color-grey-50 !default;
$ibo-caselog-entry-form--lock-icon--background-color: $ibo-color-grey-800 !default;
$ibo-caselog-entry-form--lock-icon--border-radius: $ibo-border-radius-full !default;
$ibo-caselog-entry-form--lock-message--margin-left: 1rem !default;
.ibo-caselog-entry-form {
display: block;
width: 100%;
@@ -18,14 +25,35 @@ $ibo-caselog-entry-form--actions--margin-bottom: $ibo-caselog-entry-form--action
&.ibo-is-closed {
display: none;
}
}
.ibo-caselog-entry-form--actions{
display: flex;
justify-content: space-between;
margin-top: $ibo-caselog-entry-form--actions--margin-top;
margin-bottom: $ibo-caselog-entry-form--actions--margin-bottom;
}
.ibo-caselog-entry-form--action-buttons--main-actions > .ibo-popover-menu{
z-index: 1;
.ibo-caselog-entry-form--actions {
display: flex;
justify-content: space-between;
margin-top: $ibo-caselog-entry-form--actions--margin-top;
margin-bottom: $ibo-caselog-entry-form--actions--margin-bottom;
}
.ibo-caselog-entry-form--lock-indicator {
margin-top: $ibo-caselog-entry-form--lock-indicator--margin-top;
@extend %ibo-vertically-centered-content;
}
.ibo-caselog-entry-form--lock-icon {
width: $ibo-caselog-entry-form--lock-icon--size;
min-width: $ibo-caselog-entry-form--lock-icon--size; // To avoid icon being shrinked when message is too large
height: $ibo-caselog-entry-form--lock-icon--size;
min-height: $ibo-caselog-entry-form--lock-icon--size;
color: $ibo-caselog-entry-form--lock-icon--text-color;
background-color: $ibo-caselog-entry-form--lock-icon--background-color;
border-radius: $ibo-caselog-entry-form--lock-icon--border-radius;
@extend %ibo-fully-centered-content;
}
.ibo-caselog-entry-form--lock-message {
margin-left: $ibo-caselog-entry-form--lock-message--margin-left;
}
.ibo-caselog-entry-form--action-buttons--main-actions > .ibo-popover-menu {
z-index: 1;
}

View File

@@ -1437,6 +1437,7 @@ When associated with a trigger, each action is given an "order" number, specifyi
'UI:CurrentObjectIsLockedBy_User' => 'The object is locked since it is currently being modified by %1$s.',
'UI:CurrentObjectIsLockedBy_User_Explanation' => 'The object is currently being modified by %1$s. Your modifications cannot be submitted since they would be overwritten.',
'UI:CurrentObjectIsSoftLockedBy_User' => 'The object is currently being modified by %1$s. You\'ll be able to submit your modifications once they are done.',
'UI:CurrentObjectLockExpired' => 'The lock to prevent concurrent modifications of the object has expired.',
'UI:CurrentObjectLockExpired_Explanation' => 'The lock to prevent concurrent modifications of the object has expired. You can no longer submit your modification since other users are now allowed to modify this object.',
'UI:ConcurrentLockKilled' => 'The lock preventing modifications on the current object has been deleted.',

View File

@@ -1415,6 +1415,7 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé
'UI:CurrentObjectIsLockedBy_User' => 'L\'objet est verrouillé car il est en train d\'être modifié par %1$s.',
'UI:CurrentObjectIsLockedBy_User_Explanation' => 'L\'objet est en train d\'être modifié par %1$s. Vos modifications ne peuvent pas être acceptées car elles risquent d\'être écrasées.',
'UI:CurrentObjectIsSoftLockedBy_User' => 'L\'objet est en train d\'être modifié par %1$s. Vous pourrez envoyer vos modifications quand il/elle aura fini(e).',
'UI:CurrentObjectLockExpired' => 'Le verrouillage interdisant les modifications concurrentes a expiré.',
'UI:CurrentObjectLockExpired_Explanation' => 'Le verrouillage interdisant les modifications concurrentes a expiré. Vos modifications ne peuvent pas être acceptées car d\'autres utilisateurs peuvent modifier cet objet.',
'UI:ConcurrentLockKilled' => 'Le verrouillage en édition de l\'objet courant a été supprimé.',

View File

@@ -27,6 +27,11 @@ $(function()
datetime_format: null,
datetimes_reformat_limit: 14, // In days
transaction_id: null, // Null until the user gets the lock on the object
lock_enabled: false, // Should only be true when object mode is set to "view" and the "concurrent_lock_enabled" config. param. enabled
lock_status: null,
lock_token: null,
lock_watcher_period: 30, // Period (in seconds) between lock status update, uses the "activity_panel.lock_watcher_period" config. param.
lock_endpoint: null,
show_multiple_entries_submit_confirmation: true,
},
css_classes:
@@ -48,6 +53,8 @@ $(function()
tabs_toolbars: '[data-role="ibo-activity-panel--tabs-toolbars"]',
tab_toolbar: '[data-role="ibo-activity-panel--tab-toolbar"]',
tab_toolbar_action: '[data-role="ibo-activity-panel--tab-toolbar-action"]',
lock_hint: '[data-role="ibo-caselog-entry-form--lock-indicator"]',
lock_message: '[data-role="ibo-caselog-entry-form--lock-message"]',
caselog_tab_open_all: '[data-role="ibo-activity-panel--caselog-open-all"]',
caselog_tab_close_all: '[data-role="ibo-activity-panel--caselog-close-all"]',
activity_filter: '[data-role="ibo-activity-panel--filter"]',
@@ -78,7 +85,19 @@ $(function()
caselog: 'caselog',
transition: 'transition',
edits: 'edits',
}
},
lock_status: {
// Default, we can't be sure an object is unlocked as we only check from time to time
unknown: 'unknown',
// Current user wants the lock, we are trying to get it
request_pending: 'request_pending',
// Current user does not need the lock anymore
release_pending: 'release_pending',
// Current user has the lock
locked_by_myself: 'locked_by_myself',
// Object is locked by another user
locked_by_someone_else: 'locked_by_someone_else',
},
},
// the constructor
@@ -86,6 +105,15 @@ $(function()
this.element.addClass('ibo-activity-panel');
this._bindEvents();
// Lock
if (null === this.options.lock_status) {
this.options.lock_status = this.enums.lock_status.unknown;
}
if (true === this.options.lock_enabled) {
this._InitializeLockWatcher();
}
this._ApplyEntriesFilters();
this._UpdateMessagesCounters();
this._UpdateFiltersCheckboxesFromOptions();
@@ -287,23 +315,33 @@ $(function()
}
},
/**
* Indicate that there is a draft entry and will request lock on the object
*
* @param sCaseLogAttCode {string} Attribute code of the case log entry form being draft
* @private
*/
_onDraftEntryForm: function(sCaseLogAttCode)
{
this.element.find(this.js_selectors.tab_toggler + '[data-tab-type="' + this.enums.tab_types.caselog + '"][data-caselog-attribute-code="' + sCaseLogAttCode + '"]').addClass(this.css_classes.is_draft);
_onDraftEntryForm: function (sCaseLogAttCode) {
// Put draft indicator
this.element.find(this.js_selectors.tab_toggler+'[data-tab-type="'+this.enums.tab_types.caselog+'"][data-caselog-attribute-code="'+sCaseLogAttCode+'"]').addClass(this.css_classes.is_draft);
// Request lock
this._RequestLock();
},
/**
* Remove indication of a draft entry and will cancel the lock (acquired or pending) if no draft entry left
*
* @param sCaseLogAttCode {string} Attribute code of the case log entry form being emptied
* @private
*/
_onEmptyEntryForm: function(sCaseLogAttCode)
{
this.element.find(this.js_selectors.tab_toggler + '[data-tab-type="' + this.enums.tab_types.caselog + '"][data-caselog-attribute-code="' + sCaseLogAttCode + '"]').removeClass(this.css_classes.is_draft);
_onEmptyEntryForm: function (sCaseLogAttCode) {
// Remove draft indicator
this.element.find(this.js_selectors.tab_toggler+'[data-tab-type="'+this.enums.tab_types.caselog+'"][data-caselog-attribute-code="'+sCaseLogAttCode+'"]').removeClass(this.css_classes.is_draft);
// Cancel lock if all forms empty
if (Object.keys(this._GetEntriesFromAllForms()).length === 0) {
this._CancelLock();
}
},
_onCancelledEntryForm: function()
{
_onCancelledEntryForm: function () {
this._EmptyCaseLogsEntryForms();
this._HideCaseLogsEntryForms();
},
/**
@@ -311,8 +349,13 @@ $(function()
* been edited and the user hasn't dismiss the dialog.
* @private
*/
_onRequestSubmission: function()
{
_onRequestSubmission: function () {
// Check lock state
if (this.enums.lock_status.locked_by_myself !== this.options.lock_status) {
CombodoJSConsole.Debug('ActivityPanel: Could not submit entries, current user does not have the lock on the object');
return;
}
// If several entry forms filled, show a confirmation message
if ((true === this.options.show_multiple_entries_submit_confirmation) && (Object.keys(this._GetEntriesFromAllForms()).length > 1)) {
this._ShowEntriesSubmitConfirmation();
@@ -567,8 +610,16 @@ $(function()
_HideCaseLogsEntryForms: function () {
this.element.find(this.js_selectors.caselog_entry_form).trigger('hide_form.caselog_entry_form.itop');
this.element.find(this.js_selectors.compose_button).removeClass(this.css_classes.is_hidden);
// TODO 3.0.0: Release lock
},
/**
* Empty all case logs entry forms
* Event is triggered on the corresponding elements.
*
* @return {void}
* @private
*/
_EmptyCaseLogsEntryForms: function () {
this.element.find(this.js_selectors.caselog_entry_form).trigger('clear_entry.case_entry_form.itop');
},
_FreezeCaseLogsEntryForms: function () {
this.element.find(this.js_selectors.caselog_entry_form).trigger('enter_pending_submission_state.caselog_entry_form.itop');
@@ -727,25 +778,188 @@ $(function()
});
},
// - Helpers on object lock
/**
* Initialize the lock watcher on a regular basis
*
* @return {void}
* @private
*/
_InitializeLockWatcher: function () {
const me = this;
setInterval(function () {
me._UpdateLock();
}, this.options.lock_watcher_period * 1000);
},
/**
* Request lock on the object for the current user
*
* @return {void}
* @private
*/
_RequestLock: function () {
// Do not request lock again if we already have it or a request is already pending
// Note: This can happen when we write in several case logs
if ([this.enums.lock_status.request_pending, this.enums.lock_status.locked_by_myself].indexOf(this.options.lock_status) !== -1) {
return;
}
this.options.lock_status = this.enums.lock_status.request_pending;
this._UpdateLock();
},
/**
* Cancel the lock on the object for the current user
*
* @return {void}
* @private
*/
_CancelLock: function () {
if (this.enums.lock_status.locked_by_myself === this.options.lock_status) {
this.options.lock_status = this.enums.lock_status.release_pending;
} else {
this.options.lock_status = this.enums.lock_status.unknown;
}
this._UpdateLock();
},
/**
* Update the lock status every now and then to inform the user that he/she can submit or not yet.
*
* This is to prevent scenario where the user has the lock, puts its computer in standby, opens it again after a few days
* (eg. the weekend). We have to check if he/she still has the lock or not.
*
* @return {void}
* @private
*/
_UpdateLock: function () {
const me = this;
let oParams = {
obj_class: this._GetHostObjectClass(),
obj_key: this._GetHostObjectID(),
};
// Try to acquire it if requested...
if (this.enums.lock_status.request_pending === this.options.lock_status) {
oParams.operation = 'acquire_lock';
}
// ... or extend lock if locked by current user...
else if (this.enums.lock_status.locked_by_myself === this.options.lock_status) {
oParams.operation = 'extend_lock';
oParams.token = this.options.lock_token;
}
// ... or release lock if current user does not want it anymore...
else if (this.enums.lock_status.release_pending === this.options.lock_status) {
oParams.operation = 'release_lock';
oParams.token = this.options.lock_token;
}
// ... otherwise, just check if locked by someone else
else {
oParams.operation = 'check_lock_state';
}
$.post(
this.options.lock_endpoint,
oParams,
'json'
)
.fail(function (oXHR, sStatus, sErrorThrown) {
// TODO 3.0.0: Maybe we could have a centralized dialog to display error messages?
alert(sErrorThrown);
})
.done(function (oData) {
let sNewLockStatus = me.enums.lock_status.unknown;
let sMessage = null;
// Tried to acquire lock
if ('acquire_lock' === oParams.operation) {
// Status true means that we acquired the lock...
if (true === oData.success) {
me.options.lock_token = oData.token
sNewLockStatus = me.enums.lock_status.locked_by_myself;
}
// ... otherwise we will retry later
else {
sNewLockStatus = me.enums.lock_status.request_pending;
if (oData.message) {
sMessage = oData.message;
}
}
}
// Tried to extend our lock
else if ('extend_lock' === oParams.operation) {
// Status false means that we don't have the lock anymore
if (false === oData.status) {
sMessage = oData.message;
// If it was lost, means that someone else has it, else it expired
if ('lost' === oData.operation) {
sNewLockStatus = me.enums.lock_status.locked_by_someone_else;
}
} else {
sNewLockStatus = me.enums.lock_status.locked_by_myself;
}
}
// Tried to release our lock
else if ('release_lock' === oParams.operation) {
sNewLockStatus = me.enums.lock_status.unknown;
}
// Just checked if object was locked
else if ('check_lock_state' === oParams.operation) {
if (true === oData.locked) {
sNewLockStatus = me.enums.lock_status.locked_by_someone_else;
sMessage = oData.message;
}
}
me._UpdateLockDependencies(sNewLockStatus, sMessage);
});
},
/**
* Update the lock dependencies (status, message, case logs form entries, ...)
*
* @param sNewLockStatus {string} See this.enums.lock_status
* @param sMessage {null|string}
* @return {bool} Whether the dependencies have been updated or not
* @private
*/
_UpdateLockDependencies: function (sNewLockStatus, sMessage) {
const sOldLockStatus = this.options.lock_status;
if (sOldLockStatus === sNewLockStatus) {
return false;
}
// Update lock indicator
this.options.lock_status = sNewLockStatus;
this.element.find(this.js_selectors.lock_message).text(sMessage);
const sCallback = ([this.enums.lock_status.request_pending, this.enums.lock_status.locked_by_someone_else].indexOf(sNewLockStatus) !== -1) ? 'removeClass' : 'addClass';
this.element.find(this.js_selectors.lock_hint)[sCallback](this.css_classes.is_hidden);
// Update case logs entry forms
const sEvent = (this.enums.lock_status.locked_by_myself === this.options.lock_status) ? 'enable_submission.caselog_entry_form.itop' : 'disable_submission.caselog_entry_form.itop';
this.element.find(this.js_selectors.caselog_entry_form).trigger(sEvent);
return true;
},
// - Helpers on messages
_OpenMessage: function(oEntryElem)
{
_OpenMessage: function (oEntryElem) {
oEntryElem.removeClass(this.css_classes.is_closed);
},
_OpenAllMessages: function(sCaseLogAttCode = null)
{
_OpenAllMessages: function (sCaseLogAttCode = null) {
this._SwitchAllMessages('open', sCaseLogAttCode);
},
_CloseAllMessages: function(sCaseLogAttCode = null)
{
_CloseAllMessages: function (sCaseLogAttCode = null) {
this._SwitchAllMessages('close', sCaseLogAttCode);
},
_SwitchAllMessages: function(sMode, sCaseLogAttCode = null)
{
const sExtraSelector = (sCaseLogAttCode === null) ? '' : '[data-entry-caselog-attribute-code="' + sCaseLogAttCode+'"]';
_SwitchAllMessages: function (sMode, sCaseLogAttCode = null) {
const sExtraSelector = (sCaseLogAttCode === null) ? '' : '[data-entry-caselog-attribute-code="'+sCaseLogAttCode+'"]';
const sCallback = (sMode === 'open') ? 'removeClass' : 'addClass';
this.element.find(this.js_selectors.entry + sExtraSelector)[sCallback](this.css_classes.is_closed);
this.element.find(this.js_selectors.entry+sExtraSelector)[sCallback](this.css_classes.is_closed);
},
/**
* Update the messages and users counters in the tabs toolbar

View File

@@ -97,11 +97,7 @@ $(function() {
if (bWasDraftBefore !== bIsDraftNow) {
me.is_draft = bIsDraftNow;
me._UpdateEditingVisualHint();
// Update button only once, not at each character change
if (me._IsSubmitAutonomous()) {
me._UpdateSubmitButtonState();
}
// Note: We must not call me._UpdateSubmitButtonState() as it will be updated by the disable_submission/enable_submission events
}
});
}
@@ -139,6 +135,14 @@ $(function() {
me._HideEntryForm();
});
// Form enable/disable submission
this.element.on('disable_submission.caselog_entry_form.itop', function () {
me._DisableSubmission();
});
this.element.on('enable_submission.caselog_entry_form.itop', function () {
me._EnableSubmission();
});
// Form pending submission states
this.element.on('enter_pending_submission_state.caselog_entry_form.itop', function () {
me._EnterPendingSubmissionState();
@@ -176,6 +180,12 @@ $(function() {
// TODO 3.0.0: This should also clear the form (input, lock, send button, ...)
},
_DisableSubmission: function () {
this.element.find(this.js_selectors.save_button).prop('disabled', true);
},
_EnableSubmission: function () {
this.element.find(this.js_selectors.save_button).prop('disabled', false);
},
_EnterPendingSubmissionState: function () {
this._GetCKEditorInstance().setReadOnly(true);
this.element.find(this.js_selectors.cancel_button).prop('disabled', true);
@@ -243,6 +253,7 @@ $(function() {
// - Input zone
_EmptyInput: function() {
this._GetCKEditorInstance().setData('');
this._UpdateEditingVisualHint();
},
/**
* @returns {boolean} True if the input has no text

View File

@@ -2359,8 +2359,7 @@ EOF
if ($token !== null)
{
$oExporter = BulkExport::FindExporterFromToken($token);
if ($oExporter)
{
if ($oExporter) {
$oExporter->Cleanup();
}
}
@@ -2368,32 +2367,83 @@ EOF
$oPage->add(json_encode($aResult));
break;
case 'check_lock_state':
$sObjClass = utils::ReadParam('obj_class', '', false, 'class');
$iObjKey = (int)utils::ReadParam('obj_key', 0, false, 'integer');
$aLockData = iTopOwnershipLock::IsLocked($sObjClass, $iObjKey);
$aResult = [
'locked' => $aLockData['locked'],
'message' => '',
];
// If lock taken by someone else, tell by who
if (true === $aLockData['locked']) {
// Either the contact friendlyname if the user has a contact, otherwise its login
$sOwner = ($aLockData['owner']->Get('contactid') > 0) ? $aLockData['owner']->Get('contactid_friendlyname') : $aLockData['owner']->GetRawName();
$aResult['message'] = Dict::Format('UI:CurrentObjectIsSoftLockedBy_User', $sOwner);
}
$oPage->SetContentType('application/json');
$oPage->add(json_encode($aResult));
break;
// Important: Only from the backoffice AND logged in
case 'acquire_lock':
$sObjClass = utils::ReadParam('obj_class', '', false, 'class');
$iObjKey = (int)utils::ReadParam('obj_key', 0, false, 'integer');
$aResult = iTopOwnershipLock::AcquireLock($sObjClass, $iObjKey);
if (false === $aResult['success']) {
$aLockData = iTopOwnershipLock::IsLocked($sObjClass, $iObjKey);
// If lock taken by someone else, tell by who
if (true === $aLockData['locked']) {
// Either the contact friendlyname if the user has a contact, otherwise its login
$sOwner = ($aLockData['owner']->Get('contactid') > 0) ? $aLockData['owner']->Get('contactid_friendlyname') : $aLockData['owner']->GetRawName();
$aResult['message'] = Dict::Format('UI:CurrentObjectIsSoftLockedBy_User', $sOwner);
}
}
$oPage->SetContentType('application/json');
$oPage->add(json_encode($aResult));
break;
case 'extend_lock':
$sObjClass = utils::ReadParam('obj_class', '', false, 'class');
$iObjKey = (int)utils::ReadParam('obj_key', 0, false, 'integer');
$sToken = utils::ReadParam('token', 0, false, 'raw_data');
$aResult = iTopOwnershipLock::ExtendLock($sObjClass, $iObjKey, $sToken);
if (!$aResult['status'])
{
if ($aResult['operation'] == 'lost')
{
if (!$aResult['status']) {
if ($aResult['operation'] == 'lost') {
$sName = $aResult['owner']->GetName();
if ($aResult['owner']->Get('contactid') != 0)
{
if ($aResult['owner']->Get('contactid') != 0) {
$sName .= ' ('.$aResult['owner']->Get('contactid_friendlyname').')';
}
$aResult['message'] = Dict::Format('UI:CurrentObjectIsLockedBy_User', $sName);
$aResult['popup_message'] = Dict::Format('UI:CurrentObjectIsLockedBy_User_Explanation', $sName);
}
else
{
if ($aResult['operation'] == 'expired')
{
} else {
if ($aResult['operation'] == 'expired') {
$aResult['message'] = Dict::S('UI:CurrentObjectLockExpired');
$aResult['popup_message'] = Dict::S('UI:CurrentObjectLockExpired_Explanation');
}
}
}
$oPage->SetContentType('application/json');
$oPage->add(json_encode($aResult));
break;
case 'release_lock':
$sObjClass = utils::ReadParam('obj_class', '', false, 'class');
$iObjKey = (int)utils::ReadParam('obj_key', 0, false, 'integer');
$sToken = utils::ReadParam('token', 0, false, 'raw_data');
$bReleased = iTopOwnershipLock::ReleaseLock($sObjClass, $iObjKey, $sToken);
$aResult = [
'success' => $bReleased,
];
$oPage->SetContentType('application/json');
$oPage->add(json_encode($aResult));
break;

View File

@@ -216,6 +216,17 @@ class ActivityPanel extends UIBlock
return $this->sTransactionId;
}
/**
* @return bool True if the lock mechanism has to be enabled
* @uses \cmdbAbstractObject::ENUM_OBJECT_MODE_VIEW
* @uses static::HasAnEditableCaseLogTab()
* @uses "concurrent_lock_enabled" config. param.
*/
public function IsLockEnabled(): bool
{
return (cmdbAbstractObject::ENUM_OBJECT_MODE_VIEW === $this->sObjectMode) && (MetaModel::GetConfig()->Get('concurrent_lock_enabled')) && (true === $this->HasAnEditableCaseLogTab());
}
/**
* Set all entries at once.
*
@@ -645,9 +656,19 @@ class ActivityPanel extends UIBlock
public function GetDateTimeFormatForJSWidget()
{
$oDateTimeFormat = AttributeDateTime::GetFormat();
return $oDateTimeFormat->ToMomentJS();
}
/**
* @return string The endpoint for all "lock" related operations
* @throws \Exception
*/
public function GetLockEndpointForJSWidget(): string
{
return utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php';
}
/**
* @inheritdoc
*/
@@ -655,7 +676,7 @@ class ActivityPanel extends UIBlock
{
$aSubBlocks = array();
foreach($this->GetCaseLogTabsEntryForms() as $sCaseLogId => $oCaseLogEntryForm) {
foreach ($this->GetCaseLogTabsEntryForms() as $sCaseLogId => $oCaseLogEntryForm) {
$aSubBlocks[$oCaseLogEntryForm->GetId()] = $oCaseLogEntryForm;
}

View File

@@ -23,4 +23,10 @@
<div class="ibo-caselog-entry-form--text-input" data-role="ibo-caselog-entry-form--text-input">
{{ render_block(oUIBlock.GetTextInput(), {aPage: aPage}) }}
</div>
<div class="ibo-caselog-entry-form--lock-indicator ibo-is-hidden" data-role="ibo-caselog-entry-form--lock-indicator">
<span class="ibo-caselog-entry-form--lock-icon" data-role="ibo-caselog-entry-form--lock-icon">
<span class="fas fa-lock"></span>
</span>
<span class="ibo-caselog-entry-form--lock-message" data-role="ibo-caselog-entry-form--lock-message"></span>
</div>
</form>

View File

@@ -1,5 +1,8 @@
$('#{{ oUIBlock.GetId() }}').activity_panel({
datetime_format: {{ oUIBlock.GetDateTimeFormatForJSWidget()|json_encode|raw }},
{% 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 }},
show_multiple_entries_submit_confirmation: {{ oUIBlock.GetShowMultipleEntriesSubmitConfirmation()|var_export }}
});