mirror of
https://github.com/Combodo/iTop.git
synced 2026-04-23 02:28:44 +02:00
N°3649 - Activity panel: Add concurrent lock mechanism
This commit is contained in:
@@ -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"',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.',
|
||||
|
||||
@@ -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é.',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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 }}
|
||||
});
|
||||
Reference in New Issue
Block a user