diff --git a/include/class.mailer.php b/include/class.mailer.php index c398df9f5ccf26f8bca4a8d16a21009fbc52cdf6..0abb652469fa8d6491dcd5efa1f37ac5bc139664 100644 --- a/include/class.mailer.php +++ b/include/class.mailer.php @@ -361,12 +361,12 @@ class Mailer { 'References' => $options['thread']->getEmailReferences() ); } - elseif ($parent = $options['thread']->getParent()) { + elseif ($original = $options['thread']->findOriginalEmailMessage()) { // Use the parent item as the email information source. This // will apply for staff replies $headers += array( - 'In-Reply-To' => $parent->getEmailMessageId(), - 'References' => $parent->getEmailReferences(), + 'In-Reply-To' => $original->getEmailMessageId(), + 'References' => $original->getEmailReferences(), ); } diff --git a/include/class.thread.php b/include/class.thread.php index 759a03b0c5ccc4948ee920b53e34a3c981d306a6..2b1d8bec7963775e21de4a6e4a67c2c0ede44286 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -435,6 +435,7 @@ class ThreadEntry extends VerySimpleModel { const FLAG_ORIGINAL_MESSAGE = 0x0001; const FLAG_EDITED = 0x0002; const FLAG_HIDDEN = 0x0004; + const FLAG_GUARDED = 0x0008; // No replace on edit const PERM_EDIT = 'thread.edit'; @@ -471,8 +472,7 @@ class ThreadEntry extends VerySimpleModel { } function getParent() { - if ($this->getPid()) - return ThreadEntry::lookup($this->getPid()); + return $this->parent; } function getType() { @@ -579,6 +579,21 @@ class ThreadEntry extends VerySimpleModel { return $recipients; } + /** + * Recurse through the ancestry of this thread entry to find the first + * thread entry which cites a email Message-ID field. + * + * Returns: + * <ThreadEntry> or null if neither this thread entry nor any of its + * ancestry contains an email header with an email Message-ID header. + */ + function findOriginalEmailMessage() { + $P = $this; + while (!$P->getEmailMessageId() + && ($P = $P->getParent())); + return $P; + } + function getUIDFromEmailReference($ref) { $info = unpack('Vtid/Vuid', diff --git a/include/class.thread_actions.php b/include/class.thread_actions.php index 121ecb8b98267fb2ad060bdb0f9c064a12167c9f..a228139fba0d6e9335146788c9bfce896c6abc46 100644 --- a/include/class.thread_actions.php +++ b/include/class.thread_actions.php @@ -60,7 +60,8 @@ class TEA_EditThreadEntry extends ThreadEntryAction { function isVisible() { // Can't edit system posts - return $this->entry->staff_id || $this->entry->user_id; + return ($this->entry->staff_id || $this->entry->user_id) + && $this->entry->type != 'R'; } function isEnabled() { @@ -108,15 +109,13 @@ JS } } - private function trigger__get() { - global $cfg; + protected function trigger__get() { + global $cfg, $thisstaff; include STAFFINC_DIR . 'templates/thread-entry-edit.tmpl.php'; } - private function trigger__post() { - global $thisstaff; - + function updateEntry($guard=false) { $old = $this->entry; $type = ($old->format == 'html') ? 'HtmlThreadEntryBody' : 'TextThreadEntryBody'; @@ -124,7 +123,7 @@ JS if ($new->getClean() == $old->body) // No update was performed - Http::response(201); + return $old; $entry = ThreadEntry::create(array( // Copy most information from the old entry @@ -134,6 +133,9 @@ JS 'type' => $old->type, 'threadId' => $old->thread_id, + // Connect the new entry to be a child of the previous + 'pid' => $old->id, + // Add in new stuff 'title' => $_POST['title'], 'body' => $new, @@ -141,31 +143,49 @@ JS )); if (!$entry) - return $this->trigger__get(); + return false; // 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)); + if ($old->flags & ThreadEntry::FLAG_EDITED + and !($old->flags & ThreadEntry::FLAG_GUARDED) + ) { + // Replace previous edit -------------------------- + $original = $old->getParent(); // Drop the previous edit, and base this edit off the original $old->delete(); $old = $original; } - // Mark the new entry as editited (but not hidden) + // Mark the new entry as edited (but not hidden) $entry->flags = ($old->flags & ~ThreadEntry::FLAG_HIDDEN) | ThreadEntry::FLAG_EDITED; + + // Guard against deletes on future edit if requested. This is done + // if an email was triggered by the last edit. In such a case, it + // should not be replace by a subsequent edit. + if ($guard) + $entry->flags |= ThreadEntry::FLAG_GUARDED; + + // Sort in the same place in the thread — XXX: Add a `sequence` id $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(); + return $entry; + } + + protected function trigger__post() { + global $thisstaff; + + if (!($entry = $this->updateEntry())) + return $this->trigger__get(); + Http::response('201', JsonDataEncoder::encode(array( 'thread_id' => $this->entry->id, 'new_id' => $entry->id, @@ -177,8 +197,8 @@ ThreadEntry::registerAction(/* trans */ 'Manage', 'TEA_EditThreadEntry'); class TEA_OrigThreadEntry extends ThreadEntryAction { static $id = 'previous'; - static $name = /* trans */ 'View Original'; - static $icon = 'undo'; + static $name = /* trans */ 'View History'; + static $icon = 'copy'; function isVisible() { // Can't edit system posts @@ -199,8 +219,76 @@ class TEA_OrigThreadEntry extends ThreadEntryAction { } private function trigger__get() { - $entry = ThreadEntry::lookup(array('pid'=>$this->entry->getId())); + $entry = $this->entry->getParent(); + if (!$entry) + Http::response(404, 'No history for this entry'); include STAFFINC_DIR . 'templates/thread-entry-view.tmpl.php'; } } ThreadEntry::registerAction(/* trans */ 'Manage', 'TEA_OrigThreadEntry'); + +class TEA_ResendThreadEntry extends TEA_EditThreadEntry { + static $id = 'resend'; + static $name = /* trans */ 'Edit and Resend'; + static $icon = 'reply-all'; + + function isVisible() { + // Can only resend replies + return $this->entry->staff_id && $this->entry->type == 'R'; + } + + protected function trigger__post() { + $resend = @$_POST['commit'] == 'resend'; + + if (!($entry = $this->updateEntry($resend))) + return $this->trigger__get(); + + if (@$_POST['commit'] == 'resend') + $this->resend($entry); + + Http::response('201', JsonDataEncoder::encode(array( + 'thread_id' => $this->entry->id, + 'new_id' => $entry->id, + 'body' => $entry->getBody()->toHtml(), + ))); + } + + function resend($response) { + global $cfg, $thisstaff; + + $vars = $_POST; + $ticket = $response->getThread()->getObject(); + + $dept = $ticket->getDept(); + + if ($thisstaff && $vars['signature'] == 'mine') + $signature = $thisstaff->getSignature(); + elseif ($vars['signature'] == 'dept' && $dept && $dept->isPublic()) + $signature = $dept->getSignature(); + else + $signature = ''; + + $variables = array( + 'response' => $response, + 'signature' => $signature, + 'staff' => $response->getStaff(), + 'poster' => $response->getStaff()); + $options = array('thread' => $response); + + if (($email=$dept->getEmail()) + && ($tpl = $dept->getTemplate()) + && ($msg=$tpl->getReplyMsgTemplate()) + ) { + $msg = $ticket->replaceVars($msg->asArray(), + $variables + array('recipient' => $ticket->getOwner())); + + $attachments = $cfg->emailAttachments() + ? $response->getAttachments() : array(); + $email->send($ticket->getOwner(), $msg['subj'], $msg['body'], + $attachments, $options); + } + // TODO: Add an option to the dialog + $ticket->notifyCollaborators($response, array('signature' => $signature)); + } +} +ThreadEntry::registerAction(/* trans */ 'Manage', 'TEA_ResendThreadEntry'); diff --git a/include/staff/templates/thread-entry-edit.tmpl.php b/include/staff/templates/thread-entry-edit.tmpl.php index 8ebeca0ac9bc24e2789333c23efccbaa5ae189e9..0f1533d70b59f1bd6aa37edebb89ec861b167fba 100644 --- a/include/staff/templates/thread-entry-edit.tmpl.php +++ b/include/staff/templates/thread-entry-edit.tmpl.php @@ -2,14 +2,30 @@ <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()); ?>"> +<form method="post" action="<?php echo $this->getAjaxUrl(true); ?>"> <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;" +<?php if ($this->entry->type == 'R') { + $signature = ''; + if (($T = $this->entry->getThread()->getObject()) instanceof Ticket) + $dept = $T->getDept(); + switch ($thisstaff->getDefaultSignatureType()) { + case 'dept': + if ($dept && $dept->canAppendSignature()) + $signature = $dept->getSignature(); + break; + case 'mine': + $signature = $thisstaff->getSignature(); + break; + } ?> + data-dept-id="<?php echo $dept->getId(); ?>" + data-signature-field="signature" + data-signature="<?php echo Format::viewableImages($signature); ?>" +<?php } ?> name="body" class="large <?php if ($cfg->isHtmlThreadEnabled() && $this->entry->format == 'html') @@ -17,16 +33,39 @@ ?>"><?php echo Format::viewableImages($this->entry->body); ?></textarea> +<?php if ($this->entry->type == 'R') { ?> +<div style="margin:10px 0;"><?php echo __('Signature'); ?>: + <label><input type="radio" name="signature" value="none" checked="checked"> <?php echo __('None');?></label> + <?php + if ($thisstaff->getSignature()) {?> + <label><input type="radio" name="signature" value="mine" + <?php echo ($info['signature']=='mine')?'checked="checked"':''; ?>> <?php echo __('My Signature');?></label> + <?php + } ?> + <?php + if ($dept && $dept->canAppendSignature()) { ?> + <label><input type="radio" name="signature" value="dept" + <?php echo ($info['signature']=='dept')?'checked="checked"':''; ?>> + <?php echo sprintf(__('Department Signature (%s)'), Format::htmlchars($dept->getName())); ?></label> + <?php + } ?> +</div> +<?php } # end of type == 'R' ?> + <hr> -<p class="full-width"> +<div 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'); ?>"> + <button type="submit" name="commit" value="save" class="button" + ><?php echo __('Save'); ?></button> +<?php if ($this->entry->type == 'R') { ?> + <button type="submit" name="commit" value="resend" class="button" + ><?php echo __('Save and Resend'); ?></button> +<?php } ?> </span> -</p> +</div> </form> diff --git a/include/staff/templates/thread-entry-view.tmpl.php b/include/staff/templates/thread-entry-view.tmpl.php index 500aefed15df408eb8762d93baf9c1393b445078..35c3489c4a871746f077940ebf92099d562ce645 100644 --- a/include/staff/templates/thread-entry-view.tmpl.php +++ b/include/staff/templates/thread-entry-view.tmpl.php @@ -2,9 +2,30 @@ <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 id="history" class="accordian"> + +<?php +$E = $entry; +do { ?> +<dt> + <a href="#"><i class="icon-copy"></i> + <strong><?php echo Format::htmlchars($E->title); ?></strong> + <em><?php if (strpos($E->updated, '0000-') === false) + echo sprintf(__('Edited on %s'), Format::datetime($E->updated)); + else + echo __('Original'); ?></em> + </a> +</dt> +<dd class="hidden"> + <div class="thread-body" style="background-color:transparent"> + <?php echo $E->getBody()->toHtml(); ?> + </div> +</dd> +<?php +} +while (($E = $E->getParent()) && $E->type == $entry->type); +?> + </div> <hr> @@ -16,3 +37,21 @@ </p> </form> + +<script type="text/javascript"> +$(function() { + var I = setInterval(function() { + var A = $('#history.accordian'); + if (!A.length) return; + clearInterval(I); + + var allPanels = $('dd', A).hide().removeClass('hidden'); + $('dt > a', A).click(function() { + $('dt', A).removeClass('active'); + allPanels.slideUp(); + $(this).parent().addClass('active').next().slideDown(); + return false; + }); + allPanels.last().show(); + }, 100); +}); diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index 8e3c73d86fa69f200c2a293f74b9139942dd7d9b..bdc09745da094d2daef7cb944830c7a4a267e9e7 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -429,7 +429,7 @@ $tcount = $ticket->getThreadEntries($types)->count(); ?>" href="#" onclick="javascript: if ($(this).hasClass('disabled')) return false; <?php echo str_replace('"', '\\"', $action->getJsStub()); ?>; return false;"> - <i class="<?php echo $action->getIcon(); ?>"></i> <?php + <i class="icon-fixed-width <?php echo $action->getIcon(); ?>"></i> <?php echo $action->getName(); ?></a></li> <?php } diff --git a/js/redactor-osticket.js b/js/redactor-osticket.js index 69dab8e76f7b1022317710e96efe13d581e4d493..5184801e648742e6da1d38b225eed0df354c8f3d 100644 --- a/js/redactor-osticket.js +++ b/js/redactor-osticket.js @@ -156,10 +156,10 @@ RedactorPlugins.signature = function() { else this.$signatureBox.hide(); $('input[name='+$el.data('signatureField')+']', $el.closest('form')) - .on('change', false, false, $.proxy(this.updateSignature, this)); + .on('change', false, false, $.proxy(this.signature.updateSignature, this)); if ($el.data('deptField')) $(':input[name='+$el.data('deptField')+']', $el.closest('form')) - .on('change', false, false, $.proxy(this.updateSignature, this)); + .on('change', false, false, $.proxy(this.signature.updateSignature, this)); // Expand on hover var outer = this.$signatureBox, inner = $('.inner', this.$signatureBox).get(0), diff --git a/scp/js/scp.js b/scp/js/scp.js index 8f99d9545c636d7b4e802521632f33c162ca1b19..aed3618a2be9c6bb4a99301f5e38303cafca4821 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -562,16 +562,25 @@ $.dialog = function (url, codes, cb, options) { queue: false, complete: function() { if (options.onshow) options.onshow(); } }); + var submit_button = null; $(document).off('.dialog'); + $(document).on('click.dialog', + '#popup input[type=submit], #popup button[type=submit]', + function(e) { submit_button = $(this); }); $(document).on('submit.dialog', '.dialog#popup form', function(e) { e.preventDefault(); - var $form = $(this); + var $form = $(this), + data = $form.serialize(); + if (submit_button) { + data += '&' + escape(submit_button.attr('name')) + '=' + + escape(submit_button.attr('value')); + } $('div#popup-loading', $popup).show() .find('h1').css({'margin-top':function() { return $popup.height()/3-$(this).height()/3}}); $.ajax({ type: $form.attr('method'), url: 'ajax.php/'+$form.attr('action').substr(1), - data: $form.serialize(), + data: data, cache: false, success: function(resp, status, xhr) { if (xhr && xhr.status && codes