From 7e6e203ab051d8a74a8fc09b18f4d2d7641a6150 Mon Sep 17 00:00:00 2001
From: Jared Hancock <jared@osticket.com>
Date: Wed, 16 Dec 2015 20:57:15 -0600
Subject: [PATCH] queue: Add date format filter, fix several UI issues

* Add relative and full formats to the filter list
* All dates in queue are database-relative
* Fix very odd rendering of conditions in queue table
* Fix "clip" truncate mode
* Re-implement background color for Priority column
* Allocate no space for hidden annotations
* Add checkboxes to queue preview for closer resemblance to ticket queue
* Add default formats to initial date columns
---
 include/class.forms.php                       |   9 +-
 include/class.queue.php                       | 152 ++++++++++++++----
 include/class.ticket.php                      |   5 +
 include/i18n/en_US/queue_column.yaml          |   6 +
 .../staff/templates/queue-preview.tmpl.php    |   8 +-
 .../staff/templates/queue-tickets.tmpl.php    |   8 +-
 6 files changed, 148 insertions(+), 40 deletions(-)

diff --git a/include/class.forms.php b/include/class.forms.php
index 054302393..774485ffc 100644
--- a/include/class.forms.php
+++ b/include/class.forms.php
@@ -2308,11 +2308,14 @@ class PriorityField extends ChoiceField {
             : $prio;
     }
 
-    function display($prio) {
+    function display($prio, &$styles=null) {
         if (!$prio instanceof Priority)
             return parent::display($prio);
-        return sprintf('<span class="fill" style="padding: 2px; background-color: %s">%s</span>',
-            $prio->getColor(), Format::htmlchars($prio->getDesc()));
+        if (is_array($styles))
+            $styles += array(
+                'background-color' => $prio->getColor()
+            );
+        return Format::htmlchars($prio->getDesc());
     }
 
     function toString($value) {
diff --git a/include/class.queue.php b/include/class.queue.php
index 8053f6937..6068f7049 100644
--- a/include/class.queue.php
+++ b/include/class.queue.php
@@ -294,8 +294,12 @@ abstract class QueueColumnAnnotation {
     /**
      * Estimate the width of the rendered annotation in pixels
      */
-    function getWidth() {
-        return 15;
+    function getWidth($row) {
+        return $this->isVisible($row) ? 25 : 0;
+    }
+
+    function isVisible($row) {
+        return true;
     }
 }
 
@@ -323,6 +327,10 @@ extends QueueColumnAnnotation {
             );
         }
     }
+
+    function isVisible($row) {
+        return $row[static::$qname] > 1;
+    }
 }
 
 class ThreadAttachmentCount
@@ -349,6 +357,10 @@ extends QueueColumnAnnotation {
                 $count);
         }
     }
+
+    function isVisible($row) {
+        return $row[static::$qname] > 0;
+    }
 }
 
 class ThreadCollaboratorCount
@@ -373,6 +385,10 @@ extends QueueColumnAnnotation {
                 $count);
         }
     }
+
+    function isVisible($row) {
+        return $row[static::$qname] > 0;
+    }
 }
 
 class OverdueFlagDecoration
@@ -388,6 +404,10 @@ extends QueueColumnAnnotation {
         if ($row['isoverdue'])
             return '<span class="Icon overdueTicket"></span>';
     }
+
+    function isVisible($row) {
+        return $row['isoverdue'];
+    }
 }
 
 class TicketSourceDecoration
@@ -426,6 +446,10 @@ extends QueueColumnAnnotation {
         if ($row['_locked'])
             return sprintf('<span class="Icon lockedTicket"></span>');
     }
+
+    function isVisible($row) {
+        return $row['_locked'];
+    }
 }
 
 class DataSourceField
@@ -540,20 +564,17 @@ class QueueColumnCondition {
         }
     }
 
-    function render($row, $text) {
+    function render($row, $text, &$styles=array()) {
         $annotation = $this->getAnnotationName();
         if ($V = $row[$annotation]) {
-            $style = array();
             foreach ($this->getProperties() as $css=>$value) {
                 $field = QueueColumnConditionProperty::getField($css);
                 $field->value = $value;
                 $V = $field->getClean();
                 if (is_array($V))
                     $V = current($V);
-                $style[] = "{$css}:{$V}";
+                $styles[$css] = $V;
             }
-            $text = sprintf('<span class="fill" style="%s">%s</span>',
-                implode(';', $style), $text);
         }
         return $text;
     }
@@ -653,6 +674,36 @@ extends ChoiceField {
     }
 }
 
+class LazyDisplayWrapper {
+    function __construct($field, $value) {
+        $this->field = $field;
+        $this->value = $value;
+        $this->safe = false;
+    }
+
+    /**
+     * Allow a filter to change the value of this to a "safe" value which
+     * will not be automatically encoded with htmlchars()
+     */
+    function changeTo($what, $safe=false) {
+        $this->field = null;
+        $this->value = $what;
+        $this->safe = $safe;
+    }
+
+    function __toString() {
+        return $this->display();
+    }
+
+    function display(&$styles=array()) {
+        if (isset($this->field))
+            return $this->field->display(
+                $this->field->to_php($this->value), $styles);
+        if ($this->safe)
+            return $this->value;
+        return Format::htmlchars($this->value);
+    }
+}
 
 /**
  * A column of a custom queue. Columns have many customizable features
@@ -743,22 +794,28 @@ extends VerySimpleModel {
         // Basic data
         $text = $this->renderBasicValue($row);
 
-        // Truncate
-        $text = $this->applyTruncate($text);
-
         // Filter
         if ($filter = $this->getFilter()) {
             $text = $filter->filter($text, $row) ?: $text;
         }
 
+        $styles = array();
+        if ($text instanceof LazyDisplayWrapper) {
+            $text = $text->display($styles);
+        }
+
+        // Truncate
+        $text = $this->applyTruncate($text, $row);
+
         // annotations and conditions
         foreach ($this->getAnnotations() as $D) {
             $text = $D->render($row, $text);
         }
         foreach ($this->getConditions() as $C) {
-            $text = $C->render($row, $text);
+            $text = $C->render($row, $text, $styles);
         }
-        return $text;
+        $style = Format::array_implode(':', ';', $styles);
+        return array($text, $style);
     }
 
     function renderBasicValue($row) {
@@ -767,26 +824,28 @@ extends VerySimpleModel {
         $primary = SavedSearch::getOrmPath($this->primary);
         $secondary = SavedSearch::getOrmPath($this->secondary);
 
-        // TODO: Consider data filter if configured
+        // Return a lazily ::display()ed value so that the value to be
+        // rendered by the field could be changed or display()ed when
+        // converted to a string.
 
         if (($F = $fields[$primary])
             && (list(,$field) = $F)
             && ($T = $field->from_query($row, $primary))
         ) {
-            return $field->display($field->to_php($T));
+            return new LazyDisplayWrapper($field, $T);
         }
         if (($F = $fields[$secondary])
             && (list(,$field) = $F)
             && ($T = $field->from_query($row, $secondary))
         ) {
-            return $field->display($field->to_php($T));
+            return new LazyDisplayWrapper($field, $T);
         }
     }
 
-    function applyTruncate($text) {
+    function applyTruncate($text, $row) {
         $offset = 0;
         foreach ($this->getAnnotations() as $a)
-            $offset += $a->getWidth();
+            $offset += $a->getWidth($row);
 
         $width = $this->width - $offset;
         switch ($this->truncate) {
@@ -795,7 +854,7 @@ extends VerySimpleModel {
                 'truncate', $width, $text);
         case 'clip':
             return sprintf('<span class="%s" style="max-width:%dpx">%s</span>',
-                'truncate clip', $width, $text);
+                'truncate bleed', $width, $text);
         default:
         case 'wrap':
             return $text;
@@ -1021,7 +1080,7 @@ abstract class QueueColumnFilter {
     static $id = null;
     static $desc = null;
 
-    static function register($filter) {
+    static function register($filter, $group) {
         if (!isset($filter::$id))
             throw new Exception('QueueColumnFilter must define $id');
         if (isset(static::$registry[$filter::$id]))
@@ -1030,20 +1089,24 @@ abstract class QueueColumnFilter {
         if (!is_subclass_of($filter, get_called_class()))
             throw new Exception('Filter must extend QueueColumnFilter');
 
-        static::$registry[$filter::$id] = $filter;
+        static::$registry[$filter::$id] = array($group, $filter);
     }
 
     static function getFilters() {
-        $base = static::$registry;
-        foreach ($base as $id=>$class) {
-            $base[$id] = __($class::$desc);
+        $list = static::$registry;
+        $base = array();
+        foreach ($list as $id=>$stuff) {
+            list($group, $class) = $stuff;
+            $base[$group][$id] = __($class::$desc);
         }
         return $base;
     }
 
     static function getInstance($id) {
-        if (isset(static::$registry[$id]))
-            return new static::$registry[$id]();
+        if (isset(static::$registry[$id])) {
+            list(, $class) = static::$registry[$id];
+            return new $class();
+        }
     }
 
     function mangleQuery($query, $column) { return $query; }
@@ -1058,7 +1121,7 @@ extends QueueColumnFilter {
 
     function filter($text, $row) {
         if ($link = $this->getLink($row))
-            return sprintf('<a href="%s">%s</a>', $link, $text);
+            return sprintf('<a style="display:inline" href="%s">%s</a>', $link, $text);
     }
 
     function mangleQuery($query, $column) {
@@ -1099,9 +1162,9 @@ extends TicketLinkFilter {
         return Organization::getLink($row['user__org_id']);
     }
 }
-QueueColumnFilter::register('TicketLinkFilter');
-QueueColumnFilter::register('UserLinkFilter');
-QueueColumnFilter::register('OrgLinkFilter');
+QueueColumnFilter::register('TicketLinkFilter', __('Link'));
+QueueColumnFilter::register('UserLinkFilter', __('Link'));
+QueueColumnFilter::register('OrgLinkFilter', __('Link'));
 
 class TicketLinkWithPreviewFilter
 extends TicketLinkFilter {
@@ -1110,11 +1173,38 @@ extends TicketLinkFilter {
 
     function filter($text, $row) {
         $link = $this->getLink($row);
-        return sprintf('<a class="preview" data-preview="#tickets/%d/preview" href="%s">%s</a>',
+        return sprintf('<a style="display: inline" class="preview" data-preview="#tickets/%d/preview" href="%s">%s</a>',
             $row['ticket_id'], $link, $text);
     }
 }
-QueueColumnFilter::register('TicketLinkWithPreviewFilter');
+QueueColumnFilter::register('TicketLinkWithPreviewFilter', __('Link'));
+
+class DateTimeFilter
+extends QueueColumnFilter {
+    static $id = 'date:full';
+    static $desc = /* @trans */ "Date and Time";
+
+    function filter($text, $row) {
+        return $text->changeTo(Format::datetime($text->value));
+    }
+}
+
+class HumanizedDateFilter
+extends QueueColumnFilter {
+    static $id = 'date:human';
+    static $desc = /* @trans */ "Relative Date and Time";
+
+    function filter($text, $row) {
+        return sprintf(
+            '<time class="relative" datetime="%s" title="%s">%s</time>',
+            date(DateTime::W3C, Misc::db2gmtime($text->value)),
+            Format::daydatetime($text->value),
+            Format::relativeTime(Misc::db2gmtime($text->value))
+        );
+    }
+}
+QueueColumnFilter::register('DateTimeFilter', __('Date Format'));
+QueueColumnFilter::register('HumanizedDateFilter', __('Date Format'));
 
 class QueueColDataConfigForm
 extends AbstractForm {
diff --git a/include/class.ticket.php b/include/class.ticket.php
index 4a424f672..417e7be77 100644
--- a/include/class.ticket.php
+++ b/include/class.ticket.php
@@ -1863,18 +1863,23 @@ implements RestrictedAccess, Threadable, Searchable {
             )),
             'created' => new DatetimeField(array(
                 'label' => __('Create Date'),
+                'configuration' => array('fromdb' => true),
             )),
             'est_duedate' => new DatetimeField(array(
                 'label' => __('Due Date'),
+                'configuration' => array('fromdb' => true),
             )),
             'reopened' => new DatetimeField(array(
                 'label' => __('Reopen Date'),
+                'configuration' => array('fromdb' => true),
             )),
             'closed' => new DatetimeField(array(
                 'label' => __('Close Date'),
+                'configuration' => array('fromdb' => true),
             )),
             'lastupdate' => new DatetimeField(array(
                 'label' => __('Last Update'),
+                'configuration' => array('fromdb' => true),
             )),
             'assignee' => new AssigneeChoiceField(array(
                 'label' => __('Assignee'),
diff --git a/include/i18n/en_US/queue_column.yaml b/include/i18n/en_US/queue_column.yaml
index 1777af857..bdc10fd76 100644
--- a/include/i18n/en_US/queue_column.yaml
+++ b/include/i18n/en_US/queue_column.yaml
@@ -41,6 +41,7 @@
   name: "Date Created"
   primary: "created"
   secondary: null
+  filter: "date:full"
   truncate: "wrap"
   annotations: "[]"
   conditions: "[]"
@@ -77,6 +78,7 @@
 - id: 7
   name: "Close Date"
   primary: "closed"
+  filter: "date:full"
   truncate: "wrap"
   annotations: "[]"
   conditions: "[]"
@@ -91,6 +93,7 @@
 - id: 9
   name: "Due Date"
   primary: "est_duedate"
+  filter: "date:human"
   truncate: "wrap"
   annotations: "[]"
   conditions: "[]"
@@ -98,6 +101,7 @@
 - id: 10
   name: "Last Updated"
   primary: "lastupdate"
+  filter: "date:full"
   truncate: "wrap"
   annotations: "[]"
   conditions: "[]"
@@ -112,6 +116,7 @@
 - id: 12
   name: "Last Message"
   primary: "thread__lastmessage"
+  filter: "date:human"
   truncate: "wrap"
   annotations: "[]"
   conditions: "[]"
@@ -119,6 +124,7 @@
 - id: 12
   name: "Last Response"
   primary: "thread__lastresponse"
+  filter: "date:human"
   truncate: "wrap"
   annotations: "[]"
   conditions: "[]"
diff --git a/include/staff/templates/queue-preview.tmpl.php b/include/staff/templates/queue-preview.tmpl.php
index 18e231160..81ae549f9 100644
--- a/include/staff/templates/queue-preview.tmpl.php
+++ b/include/staff/templates/queue-preview.tmpl.php
@@ -17,6 +17,7 @@ $columns = $queue->getColumns();
 <table class="list queue" border="0" cellspacing="1" cellpadding="2" width="940">
   <thead>
     <tr>
+      <th width="12px"></th>
 <?php
 foreach ($columns as $C) {
     echo sprintf('<th width="%s">%s</th>', $C->getWidth(),
@@ -28,9 +29,12 @@ foreach ($columns as $C) {
 <?php
 foreach ($tickets as $T) {
     echo '<tr>';
+    echo '<td><input type="checkbox" disabled="disabled" /></td>';
     foreach ($columns as $C) {
-        echo '<td class="offset">';
-        echo $C->render($T);
+        list($content, $styles) = $C->render($T);
+        $style = $styles ? 'style="'.$styles.'"' : '';
+        echo "<td $style>";
+        echo "<div $style>$content</div>";
         echo "</td>";
     }
     echo '</tr>';
diff --git a/include/staff/templates/queue-tickets.tmpl.php b/include/staff/templates/queue-tickets.tmpl.php
index 99238c950..3c05576dc 100644
--- a/include/staff/templates/queue-tickets.tmpl.php
+++ b/include/staff/templates/queue-tickets.tmpl.php
@@ -157,13 +157,13 @@ foreach ($columns as $C) {
 foreach ($tickets as $T) {
     echo '<tr>';
     if ($canManageTickets) { ?>
-        <td><input type="checkbox" name="ckb[]" /></td>
+        <td><input type="checkbox" class="ckb" name="ckb[]" /></td>
 <?php 
     }
     foreach ($columns as $C) {
-        echo '<td class="offset">';
-        echo $C->render($T);
-        echo "</td>";
+        list($contents, $styles) = $C->render($T);
+        $style = $styles ? 'style="'.$styles.'"' : '';
+        echo "<td $style><div $style>$contents</div></td>";
     }
     echo '</tr>';
 }
-- 
GitLab