From 7795085e482b31de7caee21bf324e98b34f07c90 Mon Sep 17 00:00:00 2001 From: Jared Hancock <jared@osticket.com> Date: Mon, 18 May 2015 14:52:28 -0500 Subject: [PATCH] thread: Add new look to client portal Also log client edits as an event --- assets/default/css/theme.css | 258 +++++++++++++++--- include/class.thread.php | 17 +- include/class.ticket.php | 3 +- .../client/templates/thread-entries.tmpl.php | 49 ++++ .../client/templates/thread-entry.tmpl.php | 91 ++++++ .../client/templates/thread-event.tmpl.php | 8 + include/client/view.inc.php | 48 +--- scp/css/scp.css | 81 ++++-- tickets.php | 11 +- 9 files changed, 447 insertions(+), 119 deletions(-) create mode 100644 include/client/templates/thread-entries.tmpl.php create mode 100644 include/client/templates/thread-entry.tmpl.php create mode 100644 include/client/templates/thread-event.tmpl.php diff --git a/assets/default/css/theme.css b/assets/default/css/theme.css index 8adafc32e..266801765 100644 --- a/assets/default/css/theme.css +++ b/assets/default/css/theme.css @@ -726,7 +726,7 @@ label.required, span.required { border-radius: 4px; } #reply { - margin-top: 20px; + margin-top: 5px; padding: 10px; background: #f9f9f9; border: 1px solid #ccc; @@ -855,44 +855,6 @@ a.refresh { text-align: left; padding: 3px 8px; } -#ticketThread table.response, -#ticketThread table.message { - margin-top: 10px; - border: 1px solid #aaa; - border-bottom: 2px solid #aaa; -} -#ticketThread table th { - text-align: left; - border-bottom: 1px solid #aaa; - font-size: 12px; - padding: 5px; -} -#ticketThread table th span { - font-weight: normal; - color: #888; - padding-left: 20px; -} -#ticketThread .message th { - background: #d8efff; -} -#ticketThread .response th { - background: #ddd; -} -#ticketThread .info { - padding: 2px; - background: #f9f9f9; - border-top: 1px solid #ddd; - height: 16px; - line-height: 16px; -} -#ticketThread .info a { - display: inline-block; - margin: 5px 10px 5px 0; - height: 16px; - line-height: 16px; - background-position: 0 50%; - background-repeat: no-repeat; -} .action-button { -webkit-border-radius: 3px; -moz-border-radius: 3px; @@ -1048,6 +1010,7 @@ img.sign-in-image { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + vertical-align: bottom; } .image-hover a.action-button:hover, .image-hover a.action-button { @@ -1079,3 +1042,220 @@ table.custom-data .headline { #ticketInfo h1 small { font-weight: normal; } +.thread-entry { + margin-bottom: 15px; +} +.thread-entry.avatar { + margin-left: 60px; +} +.thread-entry.response.avatar { + margin-right: 60px; + margin-left: 0; +} +.thread-entry > .avatar { + margin-left: -60px; + display:inline-block; + width:48px; + height:auto; + border-radius: 5px; +} +.thread-entry.response > .avatar { + margin-left: initial; + margin-right: -60px; +} +img.avatar { + border-radius: inherit; +} +.thread-entry .header { + padding: 8px 0.9em; + border: 1px solid #ccc; + border-color: rgba(0,0,0,0.2); + border-radius: 5px 5px 0 0; +} +.thread-entry.avatar .header:before { + position: absolute; + top: 7px; + right: -8px; + content: ''; + border-top: 8px solid transparent; + border-bottom: 8px solid transparent; + border-left: 8px solid #b0b0b0; + display: inline-block; +} +.thread-entry.avatar .header:after { + position: absolute; + top: 7px; + right: -8px; + content: ''; + border-top: 7px solid transparent; + border-bottom: 7px solid transparent; + display: inline-block; + margin-top: 1px; +} + +.thread-entry.avatar .header { + position: relative; +} + +.thread-entry.response .header { + background:#dddddd; +} +.thread-entry.avatar.response .header:after { + border-left: 7px solid #dddddd; + margin-right: 1px; +} + +.thread-entry.message .header { + background:#C3D9FF; +} +.thread-entry.avatar.message .header:before { + top: 7px; + left: -8px; + right: initial; + border-left: none; + border-right: 8px solid #CCC; +} +.thread-entry.avatar.message .header:before { + border-right-color: #9cadcc; +} +.thread-entry.avatar.message .header:after { + top: 7px; + left: -8px; + right: initial; + border-left: none; + border-right: 7px solid #c3d9ff; + margin-left: 1px; +} + +.thread-entry .header .title { + max-width: 500px; + vertical-align: bottom; + display: inline-block; + margin-left: 15px; +} + +.thread-entry .thread-body { + border: 1px solid #ddd; + border-top: none; + border-bottom:2px solid #aaa; + border-radius: 0 0 5px 5px; +} +.thread-body .attachments { + background-color: #f4faff; + margin: 0 -0.9em; + position: relative; + top: 0.9em; + padding: 0.3em 0.9em; + border-top: 1px dotted #ccc; + border-top-color: rgba(0,0,0,0.2); + border-radius: 0 0 6px 6px; +} +.thread-body .attachments .filesize { + margin-left: 0.5em; +} +.thread-body .attachment-info { + margin-right: 10px; + display: inline-block; + width: 48%; +} +.thread-body .attachment-info .filename { + max-width: 80%; + max-width: calc(100% - 70px); +} +.label { + font-size: 11px; + padding: 1px 4px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + font-weight: bold; + line-height: 14px; + color: #ffffff; + vertical-align: baseline; + white-space: nowrap; + 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; +} +.thread-event { + padding: 0px 2px 15px; + margin-left: 60px; +} +.type-icon { + border-radius: 8px; + background-color: #f4f4f4; + padding: 4px 6px; + margin-right: 5px; + text-align: center; + display: inline-block; + font-size: 1.1em; + border: 1px solid #eee; + vertical-align: top; +} +.type-icon.dark { + border-color: #666; + background-color: #949494; +} +.thread-event img.avatar { + vertical-align: middle; + border-radius: 3px; + width: auto; + max-height: 24px; + margin: -3px 3px 0; +} +.thread-event .description { + margin-left: -30px; + padding-top: 6px; + padding-left: 30px; + display: inline-block; + width: 642px; + width: calc(100% - 95px); + line-height: 1.4em; +} +.thread-event .type-icon { + position:relative; +} +.thread-event .type-icon::after { + content: ""; + border: 16px solid white; + position: absolute; + top: -3px; + bottom: 0; + left: -3px; + right: 0; + z-index: -1; +} +.thread-entry::after { + content: ""; + border-bottom: 2px solid white; + display: block; +} +.thread-entry::before { + content: ""; + display: block; + border-top: 2px solid white; +} +#ticketThread::before { + border-left: 2px dotted #ddd; + border-bottom-color: rgba(0,0,0,0.1); + position: absolute; + margin-left: 74px; + z-index: -1; + content: ""; + top: 0; + bottom: 0; + right: 0; + left: 0; +} +#ticketThread { + z-index: 0; + position: relative; + border-bottom: 2px solid #ddd; + border-bottom-color: rgba(0,0,0,0.1); +} diff --git a/include/class.thread.php b/include/class.thread.php index 38eb59fca..5b078a397 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -49,6 +49,9 @@ class Thread extends VerySimpleModel { ), ); + const MODE_STAFF = 1; + const MODE_CLIENT = 2; + var $_object; var $_collaborators; // Cache for collabs @@ -196,14 +199,15 @@ class Thread extends VerySimpleModel { return true; } // Render thread - function render($type=false) { + function render($type=false, $mode=self::MODE_STAFF) { $entries = $this->getEntries(); if ($type && is_array($type)) $entries->filter(array('type__in' => $type)); $events = $this->getEvents(); - include STAFFINC_DIR . 'templates/thread-entries.tmpl.php'; + $inc = ($mode == self::MODE_STAFF) ? STAFFINC_DIR : CLIENTINC_DIR; + include $inc . 'templates/thread-entries.tmpl.php'; } function getEntry($id) { @@ -1533,6 +1537,8 @@ class ThreadEvent extends VerySimpleModel { : 'somebody'; }, 'created' => __('Created by <b>{username}</b> {timestamp}'), + 'closed' => __('Closed by <b>{username}</b> {timestamp}'), + 'reopened' => __('Reopened by <b>{username}</b> {timestamp}'), 'edited:owner' => __('<b>{username}</b> changed ownership to {<User>data.owner} {timestamp}'), 'edited:status' => __('<b>{username}</b> changed the status to <strong>{<TicketStatus>data.status}</strong> {timestamp}'), 'overdue' => __('Flagged as overdue by the system {timestamp}'), @@ -1625,10 +1631,9 @@ class ThreadEvent extends VerySimpleModel { } function render($mode) { - if ($mode == self::MODE_STAFF) { - $event = $this; - include STAFFINC_DIR . 'templates/thread-event.tmpl.php'; - } + $inc = ($mode == self::MODE_STAFF) ? STAFFINC_DIR : CLIENTINC_DIR; + $event = $this; + include $inc . 'templates/thread-event.tmpl.php'; } static function create($ht=false) { diff --git a/include/class.ticket.php b/include/class.ticket.php index 13626636f..9f53f0a29 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -2649,7 +2649,8 @@ implements RestrictedAccess, Threadable, TemplateVariable { } } - $this->logEvent('edited', array('fields' => $changes)); + if ($changes) + $this->logEvent('edited', array('fields' => $changes)); // Reload the ticket so we can do further checking $this->reload(); diff --git a/include/client/templates/thread-entries.tmpl.php b/include/client/templates/thread-entries.tmpl.php new file mode 100644 index 000000000..d031182ca --- /dev/null +++ b/include/client/templates/thread-entries.tmpl.php @@ -0,0 +1,49 @@ +<?php +$events = $events + ->filter(array('state__in' => array('created', 'closed', 'reopened', 'edited', 'collab'))) + ->order_by('id'); +$events = $events->getIterator(); +$events->rewind(); +$event = $events->current(); + +if (count($entries)) { + // Go through all the entries and bucket them by time frame + $buckets = array(); + $rel = 0; + foreach ($entries as $i=>$E) { + // First item _always_ shows up + if ($i != 0) + // Set relative time resolution to 12 hours + $rel = Format::relativeTime(Misc::db2gmtime($E->created, false, 43200)); + $buckets[$rel][] = $E; + } + + // Go back through the entries and render them on the page + $i = 0; + foreach ($buckets as $rel=>$entries) { + // TODO: Consider adding a date boundary to indicate significant + // changes in dates between thread items. + foreach ($entries as $entry) { + // Emit all events prior to this entry + while ($event && $event->timestamp <= $entry->created) { + $event->render(ThreadEvent::MODE_CLIENT); + $events->next(); + $event = $events->current(); + } + include 'thread-entry.tmpl.php'; + } + $i++; + } +} + +// Emit all other events +while ($event) { + $event->render(ThreadEvent::MODE_CLIENT); + $events->next(); + $event = $events->current(); +} + +// This should never happen +if (count($entries) + count($events) == 0) { + echo '<p><em>'.__('No entries have been posted to this thread.').'</em></p>'; +} diff --git a/include/client/templates/thread-entry.tmpl.php b/include/client/templates/thread-entry.tmpl.php new file mode 100644 index 000000000..fe9f686e8 --- /dev/null +++ b/include/client/templates/thread-entry.tmpl.php @@ -0,0 +1,91 @@ +<?php +$entryTypes = array('M'=>'message', 'R'=>'response', 'N'=>'note'); +$user = $entry->getUser() ?: $entry->getStaff(); +$name = $user ? $user->getName() : $entry->poster; +$avatar = ''; +if ($user && ($url = $user->get_gravatar(48))) + $avatar = "<img class=\"avatar\" src=\"{$url}\"> "; +?> + +<div class="thread-entry <?php echo $entryTypes[$entry->type]; ?> <?php if ($avatar) echo 'avatar'; ?>"> +<?php if ($avatar) { ?> + <span class="<?php echo ($entry->type == 'M') ? 'pull-left' : 'pull-right'; ?> avatar"> +<?php echo $avatar; ?> + </span> +<?php } ?> + <div class="header"> + <div class="pull-right"> +<?php if ($entry->hasActions()) { + $actions = $entry->getActions(); ?> + <span class="muted-button pull-right" data-dropdown="#entry-action-more-<?php echo $entry->getId(); ?>"> + <i class="icon-caret-down"></i> + </span> + <div id="entry-action-more-<?php echo $entry->getId(); ?>" 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 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> + </div> +<?php + echo sprintf(__('<b>%s</b> posted %s'), $name, + sprintf('<time class="relative" datetime="%s" title="%s">%s</time>', + date(DateTime::W3C, Misc::db2gmtime($entry->created)), + Format::daydatetime($entry->created), + Format::relativeTime(Misc::db2gmtime($entry->created)) + ) + ); ?> + <span style="max-width:500px" class="faded title truncate"><?php + echo $entry->title; ?></span> + </span> + </div> + <div class="thread-body" id="thread-id-<?php echo $entry->getId(); ?>"> + <div><?php echo $entry->getBody()->toHtml(); ?></div> +<?php + if ($entry->has_attachments) { ?> + <div class="attachments"><?php + foreach ($entry->attachments as $A) { + if ($A->inline) + continue; + $size = ''; + if ($A->file->size) + $size = sprintf('<small class="filesize faded">%s</small>', Format::file_size($A->file->size)); +?> + <span class="attachment-info"> + <i class="icon-paperclip icon-flip-horizontal"></i> + <a class="no-pjax truncate filename" href="<?php echo $A->file->getDownloadUrl(); + ?>" download="<?php echo Format::htmlchars($A->file->name); ?>" + target="_blank"><?php echo Format::htmlchars($A->file->name); + ?></a><?php echo $size;?> + </span> +<?php } ?> + </div> +<?php } ?> + </div> +<?php + if ($urls = $entry->getAttachmentUrls()) { ?> + <script type="text/javascript"> + $('#thread-id-<?php echo $entry->getId(); ?>') + .data('urls', <?php + echo JsonDataEncoder::encode($urls); ?>) + .data('id', <?php echo $entry->getId(); ?>); + </script> +<?php + } ?> +</div> diff --git a/include/client/templates/thread-event.tmpl.php b/include/client/templates/thread-event.tmpl.php new file mode 100644 index 000000000..f630268cb --- /dev/null +++ b/include/client/templates/thread-event.tmpl.php @@ -0,0 +1,8 @@ +<div class="thread-event <?php if ($event->uid) echo 'action'; ?>"> + <span class="type-icon"> + <i class="faded icon-<?php echo $event->getIcon(); ?>"></i> + </span> + <span class="faded description"> + <?php echo $event->getDescription(); ?> + </span> +</div> diff --git a/include/client/view.inc.php b/include/client/view.inc.php index 4f96855d7..f11afbabf 100644 --- a/include/client/view.inc.php +++ b/include/client/view.inc.php @@ -126,55 +126,13 @@ foreach (DynamicFormEntry::forTicket($ticket->getId()) as $form) { </tr> </table> <br> + <div id="ticketThread"> <?php -if($ticket->getThreadCount() && ($thread=$ticket->getClientThread())) { - $threadType=array('M' => 'message', 'R' => 'response'); - foreach($thread as $entry) { - - //Making sure internal notes are not displayed due to backend MISTAKES! - if(!$threadType[$entry->type]) continue; - $poster = $entry->poster; - if($entry->type=='R' && ($cfg->hideStaffName() || !$entry->staff_id)) - $poster = ' '; - ?> - <table class="thread-entry <?php echo $threadType[$entry->type]; ?>" cellspacing="0" cellpadding="1" width="800" border="0"> - <tr><th><div> -<?php echo Format::datetime($entry->created); ?> - <span class="textra"></span> - <span><?php echo $poster; ?></span> - </div> - </th></tr> - <tr><td class="thread-body"><div><?php echo Format::clickableurls($entry->getBody()->toHtml()); ?></div></td></tr> - <?php - $urls = null; - if ($entry->has_attachments - && ($urls = $entry->getAttachmentUrls())) { ?> - <tr> - <td class="info"><?php - foreach ($entry->attachments as $A) { - if ($A->inline) continue; - $size = ''; - if ($A->file->size) - $size = sprintf('<em>(%s)</em>', - Format::file_size($A->file->size)); -?> - <i class="icon-paperclip"></i> - <a class="no-pjax" href="<?php echo $A->file->getDownloadUrl(); - ?>" download="<?php echo Format::htmlchars($A->file->name); ?>" - target="_blank"> - <?php echo Format::htmlchars($A->file->name); - ?></a><?php echo $size;?> -<?php } ?> - </td> - </tr> -<?php } ?> - </table> - <?php - } -} + $ticket->getThread()->render(array('M', 'R'), Thread::MODE_CLIENT); ?> </div> + <div class="clear" style="padding-bottom:10px;"></div> <?php if($errors['err']) { ?> <div id="msg_error"><?php echo $errors['err']; ?></div> diff --git a/scp/css/scp.css b/scp/css/scp.css index b5e9e7a31..6749ba729 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -832,9 +832,6 @@ h2 .reload { border:1px solid #f90; } - - - #ticket_actions { padding:5px; background:#eee; @@ -843,7 +840,18 @@ h2 .reload { margin:0; } .thread-entry { - margin-top: 15px; + margin-bottom: 15px; + z-index: 0; +} +.thread-entry::after { + content: ""; + border-bottom: 2px solid white; + display: block; +} +.thread-entry::before { + content: ""; + display: block; + border-top: 2px solid white; } .thread-entry.avatar { margin-left: 60px; @@ -875,20 +883,20 @@ img.avatar { .thread-entry.avatar .header:before { position: absolute; top: 7px; - right: -9px; + right: -8px; content: ''; - border-top: 9px solid transparent; - border-bottom: 9px solid transparent; - border-left: 9px solid #9cadcc; + border-top: 8px solid transparent; + border-bottom: 8px solid transparent; + border-left: 8px solid #9cadcc; display: inline-block; } .thread-entry.avatar .header:after { position: absolute; top: 7px; - right: -9px; + right: -8px; content: ''; - border-top: 8px solid transparent; - border-bottom: 8px solid transparent; + border-top: 7px solid transparent; + border-bottom: 7px solid transparent; display: inline-block; margin-top: 1px; } @@ -901,7 +909,7 @@ img.avatar { background:#C3D9FF; } .thread-entry.avatar.message .header:after { - border-left: 9px solid #C3D9FF; + border-left: 7px solid #C3D9FF; margin-right: 1px; } @@ -911,14 +919,13 @@ img.avatar { .thread-entry.avatar.response .header:before, .thread-entry.avatar.note .header:before { top: 7px; - left: -9px; + left: -8px; right: initial; border-left: none; - border-right: 9px solid #CCC; + border-right: 8px solid #CCC; } .thread-entry.note:not(.avatar) .header { background-color: #f4f4f4; - background-color: rgba(0,0,0,0.05); } .thread-entry.avatar.response .header:before { border-right-color: #ccb3af; @@ -929,10 +936,10 @@ img.avatar { .thread-entry.avatar.response .header:after, .thread-entry.avatar.note .header:after { top: 7px; - left: -9px; + left: -8px; right: initial; border-left: none; - border-right: 9px solid #FFE0B3; + border-right: 7px solid #FFE0B3; margin-left: 1px; } @@ -949,7 +956,7 @@ img.avatar { margin-left: 15px; } -.thread-body { +.thread-entry .thread-body { border: 1px solid #ddd; border-top: none; border-bottom:2px solid #aaa; @@ -2106,7 +2113,7 @@ tr.disabled th { .tab_content { position: relative; - padding: 5px 0; + margin: 5px 0; } .left-tabs { margin-left: 48px; @@ -2322,10 +2329,26 @@ td.indented { margin: auto; } -.thread-event { - padding: 15px 2px 0; +#ticket_thread::before { + border-left: 2px dotted #ddd; + border-bottom-color: rgba(0,0,0,0.1); + position: absolute; + margin-left: 74px; + z-index: -1; + content: ""; + top: 0; + bottom: 0; + right: 0; + left: 0; } -.thread-event.action { +#ticket_thread { + z-index: 0; + position: relative; + border-bottom: 2px solid #ddd; + border-bottom-color: rgba(0,0,0,0.1); +} +.thread-event { + padding: 0 2px 15px; margin-left: 60px; } .type-icon { @@ -2339,6 +2362,16 @@ td.indented { border: 1px solid #eee; vertical-align: top; } +.thread-event .type-icon::after { + content: ""; + border: 16px solid white; + position: absolute; + top: -3px; + bottom: 0; + left: -3px; + right: 0; + z-index: -1; +} .type-icon.dark { border-color: #666; background-color: #949494; @@ -2355,7 +2388,7 @@ td.indented { padding-top: 6px; padding-left: 30px; display: inline-block; - width: 822px; - width: calc(100% - 46px); + width: 772px; + width: calc(100% - 95px); line-height: 1.4em; } diff --git a/tickets.php b/tickets.php index 0c6dd8947..875fdfec4 100644 --- a/tickets.php +++ b/tickets.php @@ -49,6 +49,7 @@ if ($_POST && is_object($ticket) && $ticket->getId()) { $errors['err']=__('Access Denied. Possibly invalid ticket ID'); else { $forms=DynamicFormEntry::forTicket($ticket->getId()); + $changes = array(); foreach ($forms as $form) { $form->setSource($_POST); if (!$form->isValid()) @@ -56,11 +57,13 @@ if ($_POST && is_object($ticket) && $ticket->getId()) { } } if (!$errors) { - foreach ($forms as $f) $f->save(); + foreach ($forms as $f) { + $changes += $f->getChanges(); + $f->save(); + } + if ($changes) + $ticket->logEvent('edited', array('fields' => $changes)); $_REQUEST['a'] = null; //Clear edit action - going back to view. - $ticket->logNote(__('Ticket details updated'), sprintf( - __('Ticket details were updated by client %s <%s>'), - $thisclient->getName(), $thisclient->getEmail())); } break; case 'reply': -- GitLab