diff --git a/css/thread.css b/css/thread.css index 2001dde95a01ad6761be77a93eadd390544f494d..6fe012353f3137d0629f15d4ac73667694460c16 100644 --- a/css/thread.css +++ b/css/thread.css @@ -467,9 +467,6 @@ table.thread-entry { table.thread-entry th div span { vertical-align: middle; } -table.thread-entry th div :not(.title) { - font-weight: 600; -} table.thread-entry th div .title { font-weight: 400; } diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index 6cd63e98ad71c3603754b88e25cc78ee61d1bf4b..85a0c15165de97cc3fa784ce8b21a4dfe1c7ce29 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -780,6 +780,25 @@ class TicketsAjaxAPI extends AjaxController { return self::_changeSelectedTicketsStatus($state, $info, $errors); } + function triggerThreadAction($ticket_id, $thread_id, $action) { + $thread = ThreadEntry::lookup($thread_id, $ticket_id); + if (!$thread) + Http::response(404, 'No such ticket thread entry'); + + $valid = false; + foreach ($thread->getActions() as $group=>$list) { + foreach ($list as $name=>$A) { + if ($A->getId() == $action) { + $valid = true; break; + } + } + } + if (!$valid) + Http::response(400, 'Not a valid action for this thread'); + + $thread->triggerAction($action); + } + private function _changeSelectedTicketsStatus($state, $info=array(), $errors=array()) { $count = $_REQUEST['count'] ?: diff --git a/include/class.file.php b/include/class.file.php index caafae2bf9cab7018b6aee5d4a8503d2f7d84295..82b998a783e969b70918399f7559d0dd78d69f7c 100644 --- a/include/class.file.php +++ b/include/class.file.php @@ -208,8 +208,10 @@ class AttachmentFile { if ($bk->sendRedirectUrl('inline')) return; $this->makeCacheable(); - Http::download($this->getName(), $this->getType() ?: 'application/octet-stream', - null, 'inline'); + $type = $this->getType() ?: 'application/octet-stream'; + if (isset($_REQUEST['overridetype'])) + $type = $_REQUEST['overridetype']; + Http::download($this->getName(), $type, null, 'inline'); header('Content-Length: '.$this->getSize()); $this->sendData(false); exit(); diff --git a/include/class.thread.php b/include/class.thread.php index 71dc5525f4a1c41f5db5ed95f74f2053a5be62b5..ff5b1e7078628b8d982520c91437c5c4ac141273 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -1079,6 +1079,65 @@ class ThreadEntry { static function add($vars) { return ($entry=self::create($vars)) ? $entry->getId() : 0; } + + // Extensible thread entry actions ------------------------ + /** + * getActions + * + * Retrieve a list of possible actions. This list is shown to the agent + * via drop-down list at the top-right of the thread entry when rendered + * in the UI. + */ + function getActions() { + if (!isset($this->_actions)) { + $this->_actions = array(); + + foreach (self::$action_registry as $group=>$list) { + $T = array(); + $this->_actions[__($group)] = &$T; + foreach ($list as $id=>$action) { + $A = new $action($this); + if ($A->isVisible()) { + $T[$id] = $A; + } + } + unset($T); + } + } + return $this->_actions; + } + + function hasActions() { + foreach ($this->getActions() as $group => $list) { + if (count($list)) + return true; + } + return false; + } + + function triggerAction($name) { + foreach ($this->getActions() as $group=>$list) { + foreach ($list as $id=>$action) { + if (0 === strcasecmp($id, $name)) { + if (!$action->isEnabled()) + return false; + + $action->trigger(); + return true; + } + } + } + return false; + } + + static $action_registry = array(); + + static function registerAction($group, $action) { + if (!isset(self::$action_registry[$group])) + self::$action_registry[$group] = array(); + + self::$action_registry[$group][$action::getId()] = $action; + } } @@ -1253,7 +1312,7 @@ class HtmlThreadEntryBody extends ThreadEntryBody { function getSearchable() { // <br> -> \n - $body = preg_replace(array('`<br(\s*)?/?>`i', '`</div>`i'), "\n", $this->body); + $body = preg_replace(array('`<br(\s*)?/?>`i', '`</div>`i'), "\n", $this->body); # <?php $body = Format::htmldecode(Format::striptags($body)); return Format::searchable($body); } @@ -1558,4 +1617,69 @@ class TicketThread extends ObjectThread { )); } } + +/** + * Class: ThreadEntryAction + * + * Defines a simple action to be performed on a thread entry item, such as + * viewing the raw email headers used to generate the message, resend the + * confirmation emails, etc. + */ +abstract class ThreadEntryAction { + static $name; // Friendly, translatable name + static $id; // Unique identifier used for plumbing + static $icon = 'cog'; + + var $thread; + + function getName() { + $class = get_class($this); + return __($class::$name); + } + + static function getId() { + return static::$id; + } + + function getIcon() { + $class = get_class($this); + return 'icon-' . $class::$icon; + } + + function __construct(ThreadEntry $thread) { + $this->thread = $thread; + } + + abstract function trigger(); + + function getTicket() { + return $this->thread->getTicket(); + } + + function isEnabled() { + return $this->isVisible(); + } + function isVisible() { + return true; + } + + /** + * getJsStub + * + * Retrieves a small JavaScript snippet to insert into the rendered page + * which should, via an AJAX callback, trigger this action to be + * performed. The URL for this sort of activity is already provided for + * you via the ::getAjaxUrl() method in this class. + */ + abstract function getJsStub(); + + function getAjaxUrl($dialog=false) { + return sprintf('%stickets/%d/thread/%d/%s', + $dialog ? '#' : 'ajax.php/', + $this->thread->getTicketId(), + $this->thread->getId(), + static::getId() + ); + } +} ?> diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index e6522e7dee47fbb44e6a87aeb10e35379828b5e3..3542a663134e6ec02108d2efc2871d5cd8855c97 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -393,7 +393,8 @@ $tcount+= $ticket->getNumNotes(); /* -------- Messages & Responses & Notes (if inline)-------------*/ $types = array('M', 'R', 'N'); if(($thread=$ticket->getThreadEntries($types))) { - foreach($thread as $entry) { ?> + foreach($thread as $entry) { + $tentry = $ticket->getThreadEntry($entry['id']); ?> <table class="thread-entry <?php echo $threadTypes[$entry['type']]; ?>" cellspacing="0" cellpadding="1" width="940" border="0"> <tr> <th colspan="4" width="100%"> @@ -404,6 +405,28 @@ $tcount+= $ticket->getNumNotes(); <span style="display:inline-block;padding:0 1em" class="faded title"><?php echo Format::truncate($entry['title'], 100); ?></span> </span> +<?php if ($tentry->hasActions()) { + $actions = $tentry->getActions(); ?> + <div class="pull-right"> + <span class="action-button" data-dropdown="#entry-action-more-<?php echo $entry['id']; ?>"> + <i class="icon-caret-down"></i> + <span ><i class="icon-cog"></i></span> + </span> + <div id="entry-action-more-<?php echo $entry['id']; ?>" class="action-dropdown anchor-right"> + <ul class="title"> +<?php foreach ($actions as $group => $list) { + foreach ($list as $id => $action) { ?> + <li> + <a class="no-pjax" href="#" onclick="javascript: + <?php echo str_replace('"', '\\"', $action->getJsStub()); ?>; return false;"> + <i class="<?php echo $action->getIcon(); ?>"></i> <?php + echo $action->getName(); + ?></a></li> +<?php } + } ?> + </ul> + </div> +<?php } ?> <span class="pull-right" style="white-space:no-wrap;display:inline-block"> <span style="vertical-align:middle;" class="textra"></span> <span style="vertical-align:middle;" @@ -419,7 +442,6 @@ $tcount+= $ticket->getNumNotes(); <?php $urls = null; if($entry['attachments'] - && ($tentry = $ticket->getThreadEntry($entry['id'])) && ($urls = $tentry->getAttachmentUrls()) && ($links = $tentry->getAttachmentsLinks())) {?> <tr> diff --git a/scp/ajax.php b/scp/ajax.php index 1c313c18c4afab80c58cbb3579de2cf254a34c76..131243adeb18c39b487631bcb9c5aee7b50044c1 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -142,6 +142,7 @@ $dispatcher = patterns('', url_get('^(?P<tid>\d+)/canned-resp/(?P<cid>\w+).(?P<format>json|txt)', 'cannedResponse'), url_get('^(?P<tid>\d+)/status/(?P<status>\w+)(?:/(?P<sid>\d+))?$', 'changeTicketStatus'), url_post('^(?P<tid>\d+)/status$', 'setTicketStatus'), + url('^(?P<tid>\d+)/thread/(?P<thread_id>\d+)/(?P<action>\w+)$', 'triggerThreadAction'), url_get('^status/(?P<status>\w+)(?:/(?P<sid>\d+))?$', 'changeSelectedTicketsStatus'), url_post('^status/(?P<state>\w+)$', 'setSelectedTicketsStatus'), url_get('^(?P<tid>\d+)/tasks$', 'tasks'),