From 5570feca818f2c91f0e1cb6357e4bcda5c2701e1 Mon Sep 17 00:00:00 2001 From: Jared Hancock <jared@osticket.com> Date: Fri, 20 Mar 2015 00:03:39 -0500 Subject: [PATCH] Add concept of thread editing Threads can be edited by marking the original as hidden and setting it's PID to the id of the new entry. The new entry has cloned data from the original and sets the `updated` timestamp to reflect the time of last edit. An edited flag is added to the new entry to reflect its origin. This patch suggests that agents can edit their own posts, department managers can edit posts while the ticket is in their department, and that help desk admins can edit anything. If a post is edited more than once, only the most recent edit is kept. --- include/class.thread.php | 16 +- include/class.thread_actions.php | 161 +++++++++++++++++- .../templates/thread-entry-edit.tmpl.php | 32 ++++ .../templates/thread-entry-view.tmpl.php | 18 ++ include/staff/ticket-view.inc.php | 8 +- js/redactor-osticket.js | 3 +- scp/css/scp.css | 9 +- 7 files changed, 232 insertions(+), 15 deletions(-) create mode 100644 include/staff/templates/thread-entry-edit.tmpl.php create mode 100644 include/staff/templates/thread-entry-view.tmpl.php diff --git a/include/class.thread.php b/include/class.thread.php index 3cf930802..e715b6f46 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -82,6 +82,7 @@ class Thread extends VerySimpleModel { 'has_attachments' => SqlAggregate::COUNT('attachments', false, new Q(array('attachments__inline'=>0))) )); + $base->exclude(array('flags__hasbit'=>ThreadEntry::FLAG_HIDDEN)); if ($criteria) $base->filter($criteria); return $base; @@ -398,7 +399,8 @@ class ThreadEntry extends VerySimpleModel { static $meta = array( 'table' => THREAD_ENTRY_TABLE, 'pk' => array('id'), - 'select_related' => array('staff', 'user'), + 'select_related' => array('staff', 'user', 'email_info'), + 'ordering' => array('created'), 'joins' => array( 'thread' => array( 'constraint' => array('thread_id' => 'Thread.id'), @@ -430,6 +432,8 @@ class ThreadEntry extends VerySimpleModel { ); const FLAG_ORIGINAL_MESSAGE = 0x0001; + const FLAG_EDITED = 0x0002; + const FLAG_HIDDEN = 0x0004; var $_headers; var $_thread; @@ -1734,7 +1738,7 @@ abstract class ThreadEntryAction { static $id; // Unique identifier used for plumbing static $icon = 'cog'; - var $thread; + var $entry; function getName() { $class = get_class($this); @@ -1751,13 +1755,13 @@ abstract class ThreadEntryAction { } function __construct(ThreadEntry $thread) { - $this->thread = $thread; + $this->entry = $thread; } abstract function trigger(); function getTicket() { - return $this->thread->getTicket(); + return $this->entry->getObject(); } function isEnabled() { @@ -1791,8 +1795,8 @@ abstract class ThreadEntryAction { function getAjaxUrl($dialog=false) { return sprintf('%stickets/%d/thread/%d/%s', $dialog ? '#' : 'ajax.php/', - $this->thread->getThread()->getObjectId(), - $this->thread->getId(), + $this->entry->getThread()->getObjectId(), + $this->entry->getId(), static::getId() ); } diff --git a/include/class.thread_actions.php b/include/class.thread_actions.php index ce6ac8246..503578ec4 100644 --- a/include/class.thread_actions.php +++ b/include/class.thread_actions.php @@ -23,14 +23,13 @@ class TEA_ShowEmailHeaders extends ThreadEntryAction { static $name = /* trans */ 'View Email Headers'; static $icon = 'envelope'; - function isEnabled() { + function isVisible() { global $thisstaff; - return $thisstaff && $thisstaff->isAdmin(); - } + if (!$this->entry->getEmailHeader()) + return false; - function isVisible() { - return (bool) $this->thread->getEmailHeader(); + return $thisstaff && $thisstaff->isAdmin(); } function getJsStub() { @@ -47,9 +46,159 @@ class TEA_ShowEmailHeaders extends ThreadEntryAction { } private function trigger__get() { - $headers = $this->thread->getEmailHeader(); + $headers = $this->entry->getEmailHeader(); include STAFFINC_DIR . 'templates/thread-email-headers.tmpl.php'; } } ThreadEntry::registerAction(/* trans */ 'E-Mail', 'TEA_ShowEmailHeaders'); + +class TEA_EditThreadEntry extends ThreadEntryAction { + static $id = 'edit'; + static $name = /* trans */ 'Edit'; + static $icon = 'pencil'; + + function isVisible() { + // Can't edit system posts + return $this->entry->staff_id || $this->entry->user_id; + } + + function isEnabled() { + global $thisstaff; + + // You can edit your own posts or posts by your department members + // if your a manager, or everyone's if your an admin + return $thisstaff && ( + $thisstaff->isAdmin() + || (($T = $this->entry->getThread()->getObject()) + && $T instanceof Ticket + && $T->getDept()->getManagerId() == $thisstaff->getId() + ) + || ($this->entry->getStaffId() == $thisstaff->getId()) + ); + } + + function getJsStub() { + return sprintf(<<<JS +var url = '%s'; +$.dialog(url, [201], function(xhr, resp) { + var json = JSON.parse(resp); + if (!json || !json.thread_id) + return; + $('#thread-id-'+json.thread_id) + .attr('id', 'thread-id-' + json.new_id) + .find('div') + .html(json.body) + .closest('td') + .effect('highlight') +}, {size:'large'}); +JS + , $this->getAjaxUrl()); + } + + + function trigger() { + switch ($_SERVER['REQUEST_METHOD']) { + case 'GET': + return $this->trigger__get(); + case 'POST': + return $this->trigger__post(); + } + } + + private function trigger__get() { + global $cfg; + + include STAFFINC_DIR . 'templates/thread-entry-edit.tmpl.php'; + } + + private function trigger__post() { + global $thisstaff; + + $old = $this->entry; + $type = ($old->format == 'html') + ? 'HtmlThreadEntryBody' : 'TextThreadEntryBody'; + $new = new $type($_POST['body']); + + if ($new->getClean() == $old->body) + // No update was performed + Http::response(201); + + $entry = ThreadEntry::create(array( + // Copy most information from the old entry + 'poster' => $old->poster, + 'userId' => $old->user_id, + 'staffId' => $old->staff_id, + 'type' => $old->type, + 'threadId' => $old->thread_id, + + // Add in new stuff + 'title' => $_POST['title'], + 'body' => $new, + 'ip_address' => $_SERVER['REMOTE_ADDR'], + )); + + if (!$entry) + return $this->trigger__get(); + + // Note, anything that points to the $old entry as PID should remain + // that way for email header lookups and such to remain consistent + + if ($old->flags & ThreadEntry::FLAG_EDITED) { + // Second and further edit --------------- + $original = ThreadEntry::lookup(array('pid'=>$old->id)); + // Drop the previous edit, and base this edit off the original + $old->delete(); + $old = $original; + } + + // Mark the new entry as editited (but not hidden) + $entry->flags = ($old->flags & ~ThreadEntry::FLAG_HIDDEN) + | ThreadEntry::FLAG_EDITED; + $entry->created = $old->created; + $entry->updated = SqlFunction::NOW(); + $entry->save(); + + // Hide the old entry from the object thread + $old->pid = $entry->id; + $old->flags |= ThreadEntry::FLAG_HIDDEN; + $old->save(); + + Http::response('201', JsonDataEncoder::encode(array( + 'thread_id' => $this->entry->id, + 'new_id' => $entry->id, + 'body' => $entry->getBody()->toHtml(), + ))); + } +} +ThreadEntry::registerAction(/* trans */ 'Manage', 'TEA_EditThreadEntry'); + +class TEA_OrigThreadEntry extends ThreadEntryAction { + static $id = 'previous'; + static $name = /* trans */ 'View Original'; + static $icon = 'undo'; + + function isVisible() { + // Can't edit system posts + return $this->entry->flags & ThreadEntry::FLAG_EDITED; + } + + function getJsStub() { + return sprintf("$.dialog('%s');", + $this->getAjaxUrl() + ); + } + + function trigger() { + switch ($_SERVER['REQUEST_METHOD']) { + case 'GET': + return $this->trigger__get(); + } + } + + private function trigger__get() { + $entry = ThreadEntry::lookup(array('pid'=>$this->entry->getId())); + include STAFFINC_DIR . 'templates/thread-entry-view.tmpl.php'; + } +} +ThreadEntry::registerAction(/* trans */ 'Manage', 'TEA_OrigThreadEntry'); diff --git a/include/staff/templates/thread-entry-edit.tmpl.php b/include/staff/templates/thread-entry-edit.tmpl.php new file mode 100644 index 000000000..8ebeca0ac --- /dev/null +++ b/include/staff/templates/thread-entry-edit.tmpl.php @@ -0,0 +1,32 @@ +<h3><?php echo __('Edit Thread Entry'); ?></h3> +<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b> +<hr/> + +<form method="post" action="<?php + echo str_replace('ajax.php/','#',$this->getAjaxUrl()); ?>"> + +<input type="text" style="width:100%;font-size:14px" placeholder="<?php + echo __('Title'); ?>" name="title" value="<?php + echo Format::htmlchars($this->entry->title); ?>"/> +<hr style="height:0"/> +<textarea style="display: block; width: 100%; height: auto; min-height: 150px;" + name="body" + class="large <?php + if ($cfg->isHtmlThreadEnabled() && $this->entry->format == 'html') + echo 'richtext'; + ?>"><?php echo Format::viewableImages($this->entry->body); +?></textarea> + +<hr> +<p class="full-width"> + <span class="buttons pull-left"> + <input type="button" name="cancel" class="close" + value="<?php echo __('Cancel'); ?>"> + </span> + <span class="buttons pull-right"> + <input type="submit" name="save" + value="<?php echo __('Save Changes'); ?>"> + </span> +</p> + +</form> diff --git a/include/staff/templates/thread-entry-view.tmpl.php b/include/staff/templates/thread-entry-view.tmpl.php new file mode 100644 index 000000000..500aefed1 --- /dev/null +++ b/include/staff/templates/thread-entry-view.tmpl.php @@ -0,0 +1,18 @@ +<h3><?php echo __('Original Thread Entry'); ?></h3> +<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b> +<hr/> + +<div><strong><?php echo Format::htmlchars($entry->title); ?></strong></div> +<div class="thread-body" style="background-color:transparent"> + <?php echo $entry->getBody()->toHtml(); ?> +</div> + +<hr> +<p class="full-width"> + <span class="buttons pull-right"> + <input type="button" name="cancel" class="close" + value="<?php echo __('Close'); ?>"> + </span> +</p> + +</form> diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index e2cc27c5f..8823977da 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -434,7 +434,13 @@ $tcount = $ticket->getThreadEntries($types)->count(); </div> <?php } ?> <span style="vertical-align:middle"> - <span style="vertical-align:middle;" class="textra"></span> + <span style="vertical-align:middle;" class="textra"> + <?php if ($entry->flags & ThreadEntry::FLAG_EDITED) { ?> + <span class="label label-bare" title="<?php + echo sprintf(__('Edited on %s by %s'), Format::datetime($entry->updated), 'You'); + ?>"><?php echo __('Edited'); ?></span> + <?php } ?> + </span> <span style="vertical-align:middle;" class="tmeta faded title"><?php echo Format::htmlchars($entry->getName()); ?></span> diff --git a/js/redactor-osticket.js b/js/redactor-osticket.js index bd32edda6..69dab8e76 100644 --- a/js/redactor-osticket.js +++ b/js/redactor-osticket.js @@ -251,7 +251,8 @@ $(function() { 'file', 'table', 'link', '|', 'alignment', '|', 'horizontalrule'], 'buttonSource': !el.hasClass('no-bar'), - 'autoresize': !el.hasClass('no-bar'), + 'autoresize': !el.hasClass('no-bar') && !el.closest('.dialog').length, + 'maxHeight': el.closest('.dialog').length ? selectedSize : false, 'minHeight': selectedSize, 'focus': false, 'plugins': el.hasClass('no-bar') diff --git a/scp/css/scp.css b/scp/css/scp.css index 64735d3de..7817e7656 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -1947,7 +1947,7 @@ tr.disabled th { .label { font-size: 11px; - padding: 1px 4px 2px; + padding: 1px 4px; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; @@ -1959,6 +1959,13 @@ tr.disabled th { text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); background-color: #999999; } +.label-bare { + background-color: transparent; + background-color: rgba(0,0,0,0); + border: 1px solid #999999; + color: #999999; + text-shadow: none; +} .label-info { background-color: #3a87ad; } -- GitLab