diff --git a/core/config.class.inc.php b/core/config.class.inc.php index a73bdc3f7..430146982 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -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"', diff --git a/css/backoffice/layout/activity-panel/_caselog-entry-form.scss b/css/backoffice/layout/activity-panel/_caselog-entry-form.scss index 72a14ea2e..07e36f85b 100644 --- a/css/backoffice/layout/activity-panel/_caselog-entry-form.scss +++ b/css/backoffice/layout/activity-panel/_caselog-entry-form.scss @@ -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; } \ No newline at end of file diff --git a/dictionaries/en.dictionary.itop.ui.php b/dictionaries/en.dictionary.itop.ui.php index 9cc2271e1..13b8cc042 100644 --- a/dictionaries/en.dictionary.itop.ui.php +++ b/dictionaries/en.dictionary.itop.ui.php @@ -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.', diff --git a/dictionaries/fr.dictionary.itop.ui.php b/dictionaries/fr.dictionary.itop.ui.php index 18a8ffbda..f252a987f 100644 --- a/dictionaries/fr.dictionary.itop.ui.php +++ b/dictionaries/fr.dictionary.itop.ui.php @@ -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é.', diff --git a/js/layouts/activity-panel/activity-panel.js b/js/layouts/activity-panel/activity-panel.js index 3313e9f24..2a9b74723 100644 --- a/js/layouts/activity-panel/activity-panel.js +++ b/js/layouts/activity-panel/activity-panel.js @@ -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 diff --git a/js/layouts/activity-panel/caselog-entry-form.js b/js/layouts/activity-panel/caselog-entry-form.js index db5f8a11f..8b3cc9c25 100644 --- a/js/layouts/activity-panel/caselog-entry-form.js +++ b/js/layouts/activity-panel/caselog-entry-form.js @@ -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 diff --git a/pages/ajax.render.php b/pages/ajax.render.php index 90e8b01ce..b8c8d2874 100644 --- a/pages/ajax.render.php +++ b/pages/ajax.render.php @@ -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; diff --git a/sources/application/UI/Base/Layout/ActivityPanel/ActivityPanel.php b/sources/application/UI/Base/Layout/ActivityPanel/ActivityPanel.php index d07dcaf62..e90d8e815 100644 --- a/sources/application/UI/Base/Layout/ActivityPanel/ActivityPanel.php +++ b/sources/application/UI/Base/Layout/ActivityPanel/ActivityPanel.php @@ -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; } diff --git a/templates/base/layouts/activity-panel/caselog-entry-form/layout.html.twig b/templates/base/layouts/activity-panel/caselog-entry-form/layout.html.twig index 77d1e800f..98f2b7104 100644 --- a/templates/base/layouts/activity-panel/caselog-entry-form/layout.html.twig +++ b/templates/base/layouts/activity-panel/caselog-entry-form/layout.html.twig @@ -23,4 +23,10 @@
{{ render_block(oUIBlock.GetTextInput(), {aPage: aPage}) }}
+
+ + + + +
\ No newline at end of file diff --git a/templates/base/layouts/activity-panel/layout.js.twig b/templates/base/layouts/activity-panel/layout.js.twig index 07c195f9e..0355c151f 100644 --- a/templates/base/layouts/activity-panel/layout.js.twig +++ b/templates/base/layouts/activity-panel/layout.js.twig @@ -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 }} }); \ No newline at end of file