diff --git a/include/ajax.config.php b/include/ajax.config.php index 1d9471abac816ed0628a569f3cc94b49c05c9d22..0bf0a4420411304e02d14ae639f450ddb6bdbda6 100644 --- a/include/ajax.config.php +++ b/include/ajax.config.php @@ -38,7 +38,7 @@ class ConfigAjaxAPI extends AjaxController { list($primary_sl, $primary_locale) = explode('_', $primary); $config=array( - 'lock_time' => ($cfg->getLockTime()*3600), + 'lock_time' => ($cfg->getLockTime()*60), 'html_thread' => (bool) $cfg->isRichTextEnabled(), 'date_format' => $cfg->getDateFormat(true), 'lang' => $lang, diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index cf86c4930e0d18572abb08d9ab7363c16c39b78a..c458db823946cfaab8f59808c426975a84908361 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -101,20 +101,23 @@ class TicketsAjaxAPI extends AjaxController { } function acquireLock($tid) { - global $cfg,$thisstaff; + global $cfg, $thisstaff; if(!$tid || !is_numeric($tid) || !$thisstaff || !$cfg || !$cfg->getLockTime()) return 0; - if(!($ticket = Ticket::lookup($tid)) || !$ticket->checkStaffPerm($thisstaff)) - return $this->json_encode(array('id'=>0, 'retry'=>false, 'msg'=>__('Lock denied!'))); + if (!($ticket = Ticket::lookup($tid)) || !$ticket->checkStaffPerm($thisstaff)) + return $this->encode(array('id'=>0, 'retry'=>false, 'msg'=>__('Lock denied!'))); //is the ticket already locked? - if($ticket->isLocked() && ($lock=$ticket->getLock()) && !$lock->isExpired()) { + if ($ticket->isLocked() && ($lock=$ticket->getLock()) && !$lock->isExpired()) { /*Note: Ticket->acquireLock does the same logic...but we need it here since we need to know who owns the lock up front*/ //Ticket is locked by someone else.?? - if($lock->getStaffId()!=$thisstaff->getId()) - return $this->json_encode(array('id'=>0, 'retry'=>false, 'msg'=>__('Unable to acquire lock.'))); + if ($lock->getStaffId() != $thisstaff->getId()) + return $this->json_encode(array('id'=>0, 'retry'=>false, + 'msg' => sprintf(__('Currently locked by %s'), + $lock->getStaffName()) + )); //Ticket already locked by staff...try renewing it. $lock->renew(); //New clock baby! @@ -130,54 +133,60 @@ class TicketsAjaxAPI extends AjaxController { )); } - function renewLock($tid, $id) { + function renewLock($id, $ticketId) { global $thisstaff; - if(!$tid || !is_numeric($tid) || !$id || !is_numeric($id) || !$thisstaff) - return $this->json_encode(array('id'=>0, 'retry'=>true)); - - if (!($ticket = Ticket::lookup($tid))) - return $this->json_encode(array('id'=>0, 'retry'=>true)); - - $lock = $ticket->getLock(); - if(!$lock || !$lock->getStaffId() || $lock->isExpired()) //Said lock doesn't exist or is is expired - return self::acquireLock($tid); //acquire the lock - - if($lock->getStaffId()!=$thisstaff->getId()) //user doesn't own the lock anymore??? sorry...try to next time. - return $this->json_encode(array('id'=>0, 'retry'=>false)); //Give up... - - //Renew the lock. - $lock->renew(); //Failure here is not an issue since the lock is not expired yet.. client need to check time! - - return $this->json_encode(array('id'=>$lock->getId(), 'time'=>$lock->getTime())); + if (!$id || !is_numeric($id) || !$thisstaff) + Http::response(403, $this->encode(array('id'=>0, 'retry'=>false))); + if (!($lock = Lock::lookup($id))) + Http::response(404, $this->encode(array('id'=>0, 'retry'=>'acquire'))); + if (!($ticket = Ticket::lookup($ticketId)) || $ticket->lock_id != $lock->lock_id) + // Ticket / Lock mismatch + Http::response(400, $this->encode(array('id'=>0, 'retry'=>false))); + + if (!$lock->getStaffId() || $lock->isExpired()) + // Said lock doesn't exist or is is expired — fetch a new lock + return self::acquireLock($ticket->getId()); + + if ($lock->getStaffId() != $thisstaff->getId()) + // user doesn't own the lock anymore??? sorry...try to next time. + Http::response(403, $this->encode(array('id'=>0, 'retry'=>false, + 'msg' => sprintf(__('Currently locked by %s'), + $lock->getStaffName()) + ))); //Give up... + + // Ensure staff still has access + if (!$ticket->checkStaffPerm($thisstaff)) + Http::response(403, $this->encode(array('id'=>0, 'retry'=>false, + 'msg' => sprintf(__('You no longer have access to #%s.'), + $ticket->getNumber()) + ))); + + // Renew the lock. + // Failure here is not an issue since the lock is not expired yet.. client need to check time! + $lock->renew(); + + return $this->encode(array('id'=>$lock->getId(), 'time'=>$lock->getTime(), + 'code' => $lock->getCode())); } - function releaseLock($tid, $id=0) { + function releaseLock($id) { global $thisstaff; - if (!($ticket = Ticket::lookup($tid))) { + if (!$id || !is_numeric($id) || !$thisstaff) + Http::response(403, $this->encode(array('id'=>0, 'retry'=>true))); + if (!($lock = Lock::lookup($id))) + Http::response(404, $this->encode(array('id'=>0, 'retry'=>true))); + + // You have to own the lock + if ($lock->getStaffId() != $thisstaff->getId()) { return 0; } - - if ($id) { - // Fetch the lock from the ticket - if (!($lock = $ticket->getLock())) { - return 1; - } - // Identify the lock by the ID number - if ($lock->getId() != $id) { - return 0; - } - // You have to own the lock - if ($lock->getStaffId() != $thisstaff->getId()) { - return 0; - } - // Can't be expired - if ($lock->isExpired()) { - return 1; - } - return $lock->release() ? 1 : 0; + // Can't be expired + if ($lock->isExpired()) { + return 1; } + return $lock->release() ? 1 : 0; return Lock::removeStaffLocks($thisstaff->getId(), $ticket) ? 1 : 0; } diff --git a/include/class.ticket.php b/include/class.ticket.php index 725bf154fa82abc549c5127a40a3921b27257da3..6a6af2ce1adb3834489285c4644fa55ab9e97252 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -542,10 +542,16 @@ implements RestrictedAccess, Threadable { } function getLock() { - return $this->lock; + $lock = $this->lock; + if ($lock && !$lock->isExpired()) + return $lock; } - function acquireLock($staffId, $lockTime) { + function acquireLock($staffId, $lockTime=null) { + global $cfg; + + if (!isset($lockTime)) + $lockTime = $cfg->getLockTime(); if (!$staffId or !$lockTime) //Lockig disabled? return null; diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index 51e1fe1289ca4621fb5f54a5fd20735e31d65c56..f1a4560b720d10bcb2617ea92c6fa8e49eb413ae 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -8,10 +8,6 @@ if(!@$thisstaff->isStaff() || !$ticket->checkStaffPerm($thisstaff)) die('Access //Re-use the post info on error...savekeyboards.org (Why keyboard? -> some people care about objects than users!!) $info=($_POST && $errors)?Format::input($_POST):array(); -//Auto-lock the ticket if locking is enabled.. If already locked by the user then it simply renews. -if($cfg->getLockTime() && !$ticket->acquireLock($thisstaff->getId(),$cfg->getLockTime())) - $warn.=__('Unable to obtain a lock on the ticket'); - //Get the goodies. $dept = $ticket->getDept(); //Dept $role = $thisstaff->getRole($dept); @@ -448,7 +444,8 @@ $tcount = $ticket->getThreadEntries($types)->count(); <div id="msg_warning"><?php echo $warn; ?></div> <?php } ?> -<div class="sticky bar stop actions" id="response_options"> +<div class="sticky bar stop actions" id="response_options" +> <ul class="tabs"> <?php if ($role->hasPerm(TicketModel::PERM_REPLY)) { ?> @@ -470,7 +467,10 @@ $tcount = $ticket->getThreadEntries($types)->count(); </ul> <?php if ($role->hasPerm(TicketModel::PERM_REPLY)) { ?> - <form id="reply" class="tab_content spellcheck" action="tickets.php?id=<?php + <form id="reply" class="tab_content spellcheck exclusive" + data-lock-object-id="ticket/<?php echo $ticket->getId(); ?>" + data-lock-id="<?php echo ($mylock) ? $mylock->getId() : ''; ?>" + action="tickets.php?id=<?php echo $ticket->getId(); ?>" name="reply" method="post" enctype="multipart/form-data"> <?php csrf_token(); ?> <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>"> @@ -1024,19 +1024,5 @@ $(function() { } }); }); -<?php - // Set the lock if one exists - if ($mylock) { ?> -!function() { - var setLock = setInterval(function() { - if (typeof(window.autoLock) === 'undefined') - return; - clearInterval(setLock); - autoLock.setLock({ - id:<?php echo $mylock->getId(); ?>, - time: <?php echo $cfg->getLockTime() * 60; ?>}, 'acquire'); - }, 50); -}(); -<?php } ?> }); </script> diff --git a/js/redactor-osticket.js b/js/redactor-osticket.js index 8c3a8d806a98c199f2a81152fd847d21d5ae6740..3b8529754661338e6cbb07430e451161313ab99b 100644 --- a/js/redactor-osticket.js +++ b/js/redactor-osticket.js @@ -68,15 +68,6 @@ RedactorPlugins.draft = function() { this.$box.trigger('draft:recovered'); }, afterUpdateDraft: function(name, data) { - // Slight workaround. Signal the 'keyup' event normally signaled - // from typing in the <textarea> - if ($.autoLock - && this.$box.closest('form').find('input[name=lockCode]').val() - && this.code.get() - ) { - $.autoLock.handleEvent(); - } - // If the draft was created, a draft_id will be sent back — update // the URL to send updates in the future if (!this.opts.draftId && data.draft_id) { @@ -145,6 +136,19 @@ RedactorPlugins.draft = function() { }; }; +RedactorPlugins.autolock = function() { + return { + init: function() { + var code = this.$box.closest('form').find('[name=lockCode]'), + self = this; + if (code.length) + this.opts.keydownCallback = function(e) { + self.$box.closest('[data-lock-object-id]').exclusive('acquire'); + }; + } + }; +} + RedactorPlugins.signature = function() { return { init: function() { @@ -216,16 +220,6 @@ RedactorPlugins.signature = function() { } }; -RedactorPlugins.autolock = function() { - return { - init: function() { - var code = this.$box.closest('form').find('[name=lockCode]'); - if ($.autoLock && code.length) - this.opts.keydownCallback = $.autoLock.handleEvent; - } - }; -} - /* Redactor richtext init */ $(function() { var captureImageSizes = function(html) { diff --git a/scp/ajax.php b/scp/ajax.php index c2ff371d5aa6a898a05036bc23d9c20f3221f565..5df6b35b966831f028f5e6c2e59c6653e91a55b1 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -137,15 +137,17 @@ $dispatcher = patterns('', url_get('^/(?P<id>\d+)/forms/manage$', 'manageForms'), url_post('^/(?P<id>\d+)/forms/manage$', 'updateForms') )), + url('^/lock/', patterns('ajax.tickets.php:TicketsAjaxAPI', + url_post('^ticket/(?P<tid>\d+)$', 'acquireLock'), + url_post('^(?P<id>\d+)/ticket/(?P<tid>\d+)/renew', 'renewLock'), + url_post('^(?P<id>\d+)/release', 'releaseLock') + )), url('^/tickets/', patterns('ajax.tickets.php:TicketsAjaxAPI', url_get('^(?P<tid>\d+)/change-user$', 'changeUserForm'), url_post('^(?P<tid>\d+)/change-user$', 'changeUser'), url_get('^(?P<tid>\d+)/user$', 'viewUser'), url_post('^(?P<tid>\d+)/user$', 'updateUser'), url_get('^(?P<tid>\d+)/preview', 'previewTicket'), - url_post('^(?P<tid>\d+)/lock$', 'acquireLock'), - url_post('^(?P<tid>\d+)/lock/(?P<id>\d+)/renew', 'renewLock'), - url_post('^(?P<tid>\d+)/lock/(?P<id>\d+)/release', 'releaseLock'), url_get('^(?P<tid>\d+)/forms/manage$', 'manageForms'), url_post('^(?P<tid>\d+)/forms/manage$', 'updateForms'), url_get('^(?P<tid>\d+)/canned-resp/(?P<cid>\w+).(?P<format>json|txt)', 'cannedResponse'), diff --git a/scp/css/scp.css b/scp/css/scp.css index f01bbcba4da8aec6bed59039d536b8b68edbd28b..37b30ef848068e9dbc9b087bd3204de4ea960b27 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -1831,6 +1831,11 @@ button[type=submit]:hover, input[type=submit]:hover, input[type=submit]:active { background-color: rgba(0, 0, 0, 0.5); } +.button:disabled, .action-button:disabled, +button[type=submit]:disabled, input[type=submit]:disabled { + opacity: 0.6; +} + .save.pending:hover { box-shadow: 0 0 0 2px rgba(242, 165, 0, 1) inset; background-color: rgba(255, 174, 0, 0.79); @@ -2502,6 +2507,59 @@ td.indented { } } +.message.bar { + position: fixed; + top: 0; + left: 0; + right: 0; + padding: 9px 15px; + z-index: 10; + background-color: white; + box-shadow: 0 3px 10px rgba(0,0,0,0.2); + opacity: 0.95; +} +.message.bar.bottom { + bottom: 0; + top: auto; + box-shadow: 0 -3px 10px rgba(0,0,0,0.2); +} +.message.bar .avatar { + display: inline-block; + width: 36px; + height: 36px; + margin-right: 10px; + background-image: url(../images/oscar-avatars.png); + background-repeat: no-repeat; + background-size: 180px 72px; +} +.avatar.oscar-boy { + background-position: -72px 0; +} +.avatar.oscar-borg { + background-position: 0 -36px; +} +.message.bar .title { + font-weight: bold; + font-size: 1.1em; +} +.message.bar .body { + margin-left: 42px; +} +.message.bar.warning { + border-bottom: 3px solid orange; +} +.message.bar.bottom.warning { + border-bottom: none; + border-top: 3px solid orange; +} +.message.bar.danger { + border-bottom: 3px solid red; +} +.message.bar.bottom.danger { + border-bottom: none; + border-top: 3px solid red; +} + #thread-items::before { border-left: 2px dotted #ddd; border-bottom-color: rgba(0,0,0,0.1); diff --git a/scp/images/oscar-avatars.png b/scp/images/oscar-avatars.png new file mode 100644 index 0000000000000000000000000000000000000000..624c0420ab336fb6391b7288b869526bf9bc55df Binary files /dev/null and b/scp/images/oscar-avatars.png differ diff --git a/scp/js/scp.js b/scp/js/scp.js index 3eca5cee3195241d5377f05db857a6d66bb5d6ab..9d72b96378aa90bebc0560349d0f42f9133fe45b 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -762,6 +762,131 @@ $.orgLookup = function (url, cb) { $.uid = 1; ++function($) { + var MessageBar = function() { + this.defaults = { + avatar: 'oscar-boy', + bar: '<div class="message bar"></div>', + button: '<button type="button" class="inline button"></button>', + buttonClass: '', + buttonText: __('OK'), + classes: '', + dismissible: true, + onok: null, + position: 'top' + }; + + this.show = function(title, message, options) { + this.hide(); + options = $.extend({}, this.defaults, options); + var bar = this.bar = $(options.bar).addClass(options.classes) + .append($('<div class="title"></div>').text(title)) + .append($('<div class="body"></div>').text(message)) + .addClass(options.position); + if (options.avatar) + bar.prepend($('<div class="avatar pull-left" title="Oscar"></div>') + .addClass(options.avatar)); + + if (options.onok || options.dismissible) { + bar + .prepend($('<div><div class="valign-helper"></div></div>') + // FIXME: This is not compatible with .rtl + .css({position: 'absolute', top: 0, bottom: 0, right: 0, margin: '0 15px'}) + .append($(options.button) + .text(options.buttonText) + .click(this.dismiss.bind(this)) + .addClass(options.buttonClass) + ) + ); + } + this.visible = true; + this.options = options; + + $('body').append(bar); + this.height = bar.height(); + + // Slight slide in + if (options.position == 'bottom') { + bar.css('bottom', -this.height/2).animate({'bottom': 0}); + } + // Otherwise assume TOP positioning + else { + var hovering = false, + y = $(window).scrollTop(), + targetY = (y < this.height) ? -this.height - 10 + y : 0; + bar.css('top', -this.height/2).animate({'top': targetY}); + + // Plop out on mouse hover + bar.hover(function() { + if (!hovering && this.visible && bar.css('top') != '0') { + bar.stop().animate({'margin-top': -parseInt(bar.css('top'), 10)}, 400, 'easeOutBounce'); + hovering = true; + } + }.bind(this), function() { + if (this.visible && hovering) { + bar.stop().animate({'margin-top': 0}); + hovering = false; + } + }.bind(this)); + } + + return bar; + }; + + this.scroll = function(event) { + // Shade on scroll to top + if (!this.visible || this.options.position != 'top') + return; + var y = $(window).scrollTop(); + if (y < this.height) { + this.bar.css({top: -this.height -10 + y}); + this.shading = true; + } + else if (this.bar.css('top') != '0') { + if (this.shading) { + this.bar.stop().animate({top: 0}); + this.shading = false; + } + } + }; + + this.dismiss = function(event) { + if (this.options.onok) { + this.bar.find('button').replaceWith( + $('<i class="icon-spinner icon-spin icon-large"></i>') + ); + if (this.options.onok(event) === false) + return; + } + this.hide(); + }; + + this.hide = function() { + if (!this.bar || !this.visible) + return; + var bar = this.bar.removeAttr('style'); + var dir = this.options.position == 'bottom' ? 'down' : 'up'; + // NOTE: destroy() is not called here because a new bar might be + // created before the animation finishes + bar.hide("slide", { direction: dir }, 400, function() { bar.remove(); }); + this.visible = false; + }; + + this.destroy = function() { + if (!this.bar || !this.visible) + return; + this.bar.remove(); + this.visible = false; + }; + + // Destroy on away navigation + $(document).on('pjax:start.messageBar', this.destroy.bind(this)); + $(window).on('scroll.messageBar', this.scroll.bind(this)); + }; + + $.messageBar = new MessageBar(); +}(window.jQuery); + // Tabs $(document).on('click.tab', 'ul.tabs > li > a', function(e) { e.preventDefault(); @@ -857,9 +982,6 @@ getConfig = (function() { })(); $(document).on('pjax:click', function(options) { - // Release ticket lock (maybe) - if ($.autoLock !== undefined) - $.autoLock.releaseLock(); // Stop all animations $(document).stop(false, true); diff --git a/scp/js/ticket.js b/scp/js/ticket.js index 4cfb20a7cc92c1055c9e7a7d41e35f780c18d882..9a54a7cee4017ed1548834e8b990c659794358e1 100644 --- a/scp/js/ticket.js +++ b/scp/js/ticket.js @@ -13,282 +13,227 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ -var autoLock = { - - // Defaults - lockId: 0, - lockCode: '', - timerId: 0, - lasteventTime: 0, - lastcheckTime: 0, - lastattemptTime: 0, - acquireTime: 0, - renewTime: 0, - renewFreq: 10000, //renewal frequency in seconds...based on returned lock time. - time: 0, - lockAttempts: 0, //Consecutive lock attempt errors - maxattempts: 2, //Maximum failed lock attempts before giving up. - warn: true, - retry: true, - - addEvent: function(elm, evType, fn, useCapture) { - if(elm.addEventListener) { - elm.addEventListener(evType, fn, useCapture); - return true; - }else if(elm.attachEvent) { - return elm.attachEvent('on' + evType, fn); - }else{ - elm['on' + evType] = fn; - } ++function( $ ) { + var Lock = function(element, options) { + this.$element = $(element); + this.options = $.extend({}, $.fn.exclusive.defaults, options); + if (!this.$element.data('lockObjectId')) + return; + this.objectId = this.$element.data('lockObjectId'); + this.lockId = options.lockId || this.$element.data('lockId') || undefined; + this.fails = 0; + this.setup(); + } + + Lock.prototype = { + constructor: Lock, + + setup: function() { + // When something inside changes or is clicked which requires a lock, + // attempt to fetch one (lazily) + $(':input', this.$element).on('keyup, change', this.acquire.bind(this)); + $(':submit', this.$element).click(this.ensureLocked.bind(this)); + + // If lock already held, assume full time of lock remains, but warn + // user about pending expiration + if (this.lockId) { + getConfig().then(function(c) { + this.lockTimeout(c.lock_time - 20); + }.bind(this)); + } }, - removeEvent: function(elm, evType, fn, useCapture) { - if(elm.removeEventListener) { - elm.removeEventListener(evType, fn, useCapture); - return true; - }else if(elm.detachEvent) { - return elm.detachEvent('on' + evType, fn); - }else { - elm['on' + evType] = null; - } - }, - - //Incoming event... - handleEvent: function(e) { - - if(autoLock.lockId && !autoLock.lasteventTime) { //I hate nav away warnings..but - $(document).on('pjax:beforeSend.changed', function(e) { - return confirm(__("Any changes or info you've entered will be discarded!")); - }); - $(window).bind('beforeunload', function(e) { - return __("Any changes or info you've entered will be discarded!"); - }); - } - // Handle events only every few seconds - var now = new Date().getTime(), - renewFreq = autoLock.renewFreq; - - if (autoLock.lasteventTime && now - autoLock.lasteventTime < renewFreq) - return; - - autoLock.lasteventTime = now; - - if (!autoLock.lockId) { - // Retry every 5 seconds?? - if (autoLock.retry) - autoLock.acquireLock(e,autoLock.warn); - } else { - autoLock.renewLock(e); - } - + acquire: function() { + if (this.lockId) + return this.renew(); + if (this.nextRenew && new Date().getTime() < this.nextRenew) + return this.locked; + if (this.ajaxActive) + return this.locked; + + this.ajaxActive = $.ajax({ + type: "POST", + url: 'ajax.php/lock/'+this.objectId, + dataType: 'json', + cache: false, + success: $.proxy(this.update, this), + error: $.proxy(this.retry, this, this.acquire), + complete: $.proxy(function() { this.ajaxActive = false; }, this) + }); + return this.locked = $.Deferred(); }, - //Watch activity on individual form. - watchForm: function(fObj,fn) { - if(!fObj || !fObj.length) - return; - - //Watch onSubmit event on the form. - autoLock.addEvent(fObj,'submit',autoLock.onSubmit,true); - //Watch activity on text + textareas + select fields. - for (var i=0; i<fObj.length; i++) { - switch(fObj[i].type) { - case 'textarea': - case 'text': - autoLock.addEvent(fObj[i],'keyup',autoLock.handleEvent,true); - break; - case 'select-one': - case 'select-multiple': - if(fObj.name!='reply') //Bug on double ajax call since select make it's own ajax call. TODO: fix it - autoLock.addEvent(fObj[i],'change',autoLock.handleEvent,true); - break; - default: - } - } + renew: function() { + if (!this.lockId) + return; + if (this.nextRenew && new Date().getTime() < this.nextRenew) + return this.locked; + if (this.ajaxActive) + return this.locked; + + this.ajaxActive = $.ajax({ + type: "POST", + url: 'ajax.php/lock/{0}/{1}/renew'.replace('{0}',this.lockId).replace('{1}',this.objectId), + dataType: 'json', + cache: false, + success: $.proxy(this.update, this), + error: $.proxy(this.retry, this, this.renew), + complete: $.proxy(function() { this.ajaxActive = false; }, this) + }); + return this.locked = $.Deferred(); }, - //Watch all the forms on the document. - watchDocument: function() { - - //Watch forms of interest only. - for (var i=0; i<document.forms.length; i++) { - if(!document.forms[i].id.value || parseInt(document.forms[i].id.value)!=autoLock.tid) - continue; - autoLock.watchForm(document.forms[i],autoLock.checkLock); - } + wakeup: function(e) { + // Click handler from message bar. Bar will be manually hidden when + // lock is re-acquired + this.renew(); + return false; }, - Init: function(config) { - - //make sure we are on ticket view page & locking is enabled! - var fObj=$('form#note'); - if(!fObj - || !$(':input[name=id]',fObj).length - || !$(':input[name=locktime]',fObj).length - || $(':input[name=locktime]',fObj).val()==0) { - return; - } - - void(autoLock.tid=parseInt($(':input[name=id]',fObj).val())); - void(autoLock.lockTime=parseInt($(':input[name=locktime]',fObj).val())); - - autoLock.watchDocument(); - autoLock.addEvent(window,'unload',autoLock.releaseLock,true); //Release lock regardless of any activity. + retry: function(func, xhr, textStatus, response) { + var json = xhr ? xhr.responseJSON : response; + + if ((typeof json == 'object' && !json.retry) || !this.options.retry) + return this.fail(json.msg); + if (typeof json == 'object' && json.retry == 'acquire') { + // Lock no longer exists server-side + this.destroy(); + setTimeout(this.acquire.bind(this), 2); + } + if (++this.fails > this.options.maxRetries) + // Attempt to acquire a new lock ? + return this.fail(json ? json.msg : null); + this.retryTimer = setTimeout($.proxy(func, this), this.options.retryInterval * 1000); }, - - onSubmit: function(e) { - if(e.type=='submit') { //Submit. double check! - //remove nav away warning if any. - $(window).unbind('beforeunload'); - //Only warn if we had a failed lock attempt. - if(autoLock.warn && !autoLock.lockId && autoLock.lasteventTime) { - var answer=confirm(__('Unable to acquire a lock on the ticket. Someone else could be working on the same ticket. Please confirm if you wish to continue anyways.')); - if(!answer) { - e.returnValue=false; - e.cancelBubble=true; - if(e.preventDefault) { - e.preventDefault(); - } - return false; - } - } - } - return true; + release: function() { + if (!this.lockId) + return false; + if (this.ajaxActive) + this.ajaxActive.abort(); + + $.ajax({ + type: 'POST', + url: 'ajax.php/lock/{0}/release'.replace('{0}', this.lockId), + data: 'delete', + async: false, + cache: false, + always: $.proxy(this.destroy, this) + }); }, - acquireLock: function(e,warn) { - - if(!autoLock.tid) { return false; } - - var warn = warn || false; - - if(autoLock.lockId) { - autoLock.renewLock(e); - } else { - $.ajax({ - type: "POST", - url: 'ajax.php/tickets/'+autoLock.tid+'/lock', - dataType: 'json', - cache: false, - success: function(lock){ - autoLock.setLock(lock,'acquire',warn); - } - }); - } - - return autoLock.lockId; + destroy: function() { + clearTimeout(this.warning); + clearTimeout(this.retryTimer); + $(window).off('.exclusive'); + delete this.lockId; + $(this.options.lockInput, this.$element).val(''); }, - //Renewal only happens on form activity.. - renewLock: function(e) { - - if (!autoLock.lockId) - return false; - - var now = new Date().getTime(), - renewFreq = autoLock.renewFreq; - - if (autoLock.lastcheckTime && now - autoLock.lastcheckTime < renewFreq) - return; - - autoLock.lastcheckTime = now; - $.ajax({ - type: 'POST', - url: 'ajax.php/tickets/'+autoLock.tid+'/lock/'+autoLock.lockId+'/renew', - dataType: 'json', - cache: false, - success: function(lock){ - autoLock.setLock(lock,'renew',autoLock.warn); - } - }); + update: function(lock) { + if (typeof lock != 'object' || lock.retry === true) { + // Non-json response, or retry requested server-side + return this.retry(this.renew, this.activeAjax, false, lock); + } + if (!lock.id) { + // Response did not include a lock id number + return this.fail(lock.msg); + } + if (!this.lockId) { + // Set up release on away navigation + $(window).off('.exclusive'); + $(window).on('pjax:click.exclusive', $.proxy(this.release, this)); + } + + this.lockId = lock.id; + this.fails = 0; + $.messageBar.hide(); + this.errorBar = false; + + // If there is an input with the name 'lockCode', then set the value + // to the lock.code retrieved (if any) + if (lock.code) + $(this.options.lockInput, this.$element).val(lock.code); + + // Deadband renew to every 30 seconds + this.nextRenew = new Date().getTime() + 30000; + + // Warn 10 seconds before expiration + this.lockTimeout(lock.time - 10); + + if (this.locked) + this.locked.resolve(lock); }, - releaseLock: function(e) { - if (!autoLock.tid || !autoLock.lockId) { return false; } - - $.ajax({ - type: 'POST', - url: 'ajax.php/tickets/'+autoLock.tid+'/lock/'+autoLock.lockId+'/release', - data: 'delete', - async: false, - cache: false, - success: function() { - autoLock.destroy(); - } - }); + lockTimeout: function(time) { + if (this.warning) + clearTimeout(this.warning); + this.warning = setTimeout(this.warn.bind(this), time * 1000); }, - setLock: function(lock, action, warn) { - var warn = warn || false; - - if (!lock) - return false; - - autoLock.lockId=lock.id; //override the lockid. - - if (lock.code) { - autoLock.lockCode = lock.code; - // Update the lock code for the upcoming POST - var el = $('input[name=lockCode]').val(lock.code); - } - - switch(action){ - case 'renew': - if(!lock.id && lock.retry) { - autoLock.lockAttempts=1; //reset retries. - autoLock.acquireLock(e,false); //We lost the lock?? ..try to re acquire now. - } - break; - case 'acquire': - if(!lock.id) { - autoLock.lockAttempts++; - if(warn && (!lock.retry || autoLock.lockAttempts>=autoLock.maxattempts)) { - autoLock.retry=false; - alert(__('Unable to lock the ticket. Someone else could be working on the same ticket.')); - } - } - break; - } - - if (lock.id && lock.time) { - autoLock.resetTimer((lock.time - 10) * 1000); - } + ensureLocked: function(e) { + // Make sure a lock code has been fetched first + if (!$(this.options.lockInput, this.$element).val()) { + var $target = $(e.target), + text = $target.text() || $target.val(); + $target.prop('disabled', true).text(__('Acquiring Lock')).val(__('Acquiring Lock')); + this.acquire().always(function(lock) { + $target.text(text).val(text).prop('disabled', false); + if (typeof lock == 'object' && lock.code) + $target.trigger(e.type, e); + }.bind(this)); + return false; + } }, - discardWarning: function(e) { - e.returnValue=__("Any changes or info you've entered will be discarded!"); + warn: function() { + $.messageBar.show( + __('Your lock is expiring soon.'), + __('The lock you hold on this ticket will expire soon. Would you like to renew the lock?'), + {onok: this.wakeup.bind(this), buttonText: __("Renew")} + ).addClass('warning'); }, - //TODO: Monitor events and elapsed time and warn user when the lock is about to expire. - monitorEvents: function() { - $.sysAlert( - __('Your lock is expiring soon'), - __('The lock you hold on this ticket will expire soon. Would you like to renew the lock?'), - function() { - autoLock.renewLock(); - } - ); - }, + fail: function(msg) { + // Don't retry for 5 seconds + this.nextRenew = new Date().getTime() + 5000; + // Resolve anything awaiting + if (this.locked) + this.locked.rejectWith(msg); + // No longer locked + this.destroy(); + // Flash the error bar if it's already on the screen + if (this.errorBar && $.messageBar.visible) + return this.errorBar.effect('highlight'); + // Add the error bar to the screen + this.errorBar = $.messageBar.show( + msg || __('Unable to lock the ticket.'), + __('Someone else could be working on the same ticket.'), + {avatar: 'oscar-borg', buttonClass: 'red', dismissible: true} + ).addClass('danger'); + } + }; + + $.fn.exclusive = function ( option ) { + return this.each(function () { + var $this = $(this), + data = $this.data('exclusive'), + options = typeof option == 'object' && option; + if (!data) $this.data('exclusive', (data = new Lock(this, options))); + if (typeof option == 'string') data[option](); + }); + }; - clearTimer: function() { - clearTimeout(autoLock.timerId); - }, + $.fn.exclusive.defaults = { + lockInput: 'input[name=lockCode]', + maxRetries: 2, + retry: true, + retryInterval: 2 + }; - resetTimer: function(time) { - autoLock.clearTimer(); - autoLock.timerId = setTimeout( - function () { autoLock.monitorEvents(); }, - time || 30000 - ); - }, + $.fn.exclusive.Constructor = Lock; - destroy: function() { - autoLock.clearTimer(); - autoLock.lockId = 0; - } -}; -$.autoLock = autoLock; +}(window.jQuery); /* UI & form events @@ -341,14 +286,13 @@ $.refreshTicketView = function(interval) { clearInterval(refresh); $.pjax({url: document.location.href, container:'#pjax-container'}); }, interval); -} +}; var ticket_onload = function($) { if (0 === $('#ticketThread').length) return; - //Start watching the form for activity. - autoLock.Init(); + $(function(){$('.exclusive[data-lock-object-id]').exclusive();}); /*** Ticket Actions **/ //print options TODO: move to backend