diff --git a/bootstrap.php b/bootstrap.php index 4b64227a839c1e9cd864db6cc74a1930e9906f98..056c5f01dcd4098d7f8649be2211249a7b9f20f8 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -293,6 +293,10 @@ class Bootstrap { } if (extension_loaded('iconv')) iconv_set_encoding('internal_encoding', 'UTF-8'); + + function mb_str_wc($str) { + return count(preg_split('~[^\p{L}\p{N}\'].+~u', trim($str))); + } } function croak($message) { diff --git a/include/ajax.search.php b/include/ajax.search.php index dbc0a291b86d3c7bebb6f1c67333735b23739b9e..fabe2d810fdd8e694d566b418b656c028e94a916 100644 --- a/include/ajax.search.php +++ b/include/ajax.search.php @@ -395,7 +395,7 @@ class SearchAjaxAPI extends AjaxController { if ($ids && is_array($ids)) $criteria = array('id__in' => $ids); - $counts = SavedQueue::ticketsCount($thisstaff, $criteria, 'q'); + $counts = SavedQueue::counts($thisstaff, $criteria); Http::response(200, false, 'application/json'); return $this->encode($counts); } diff --git a/include/class.orm.php b/include/class.orm.php index 473be838ceba796105db5770b71d1f620f895f5e..35886ad1f619298310582337a51679a931ca50ad 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -333,6 +333,11 @@ class VerySimpleModel { return static::getMeta()->newInstance($row); } + function __wakeup() { + // If a model is stashed in a session, refresh the model from the database + $this->refetch(); + } + function get($field, $default=false) { if (array_key_exists($field, $this->ht)) return $this->ht[$field]; @@ -1142,6 +1147,7 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl const OPT_NOSORT = 'nosort'; const OPT_NOCACHE = 'nocache'; const OPT_MYSQL_FOUND_ROWS = 'found_rows'; + const OPT_INDEX_HINT = 'indexhint'; const ITER_MODELS = 1; const ITER_HASH = 2; @@ -1281,6 +1287,10 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl return $this; } + function addExtraJoin(array $join) { + return $this->extra(array('joins' => array($join))); + } + function distinct() { foreach (func_get_args() as $D) $this->distinct[] = $D; @@ -1477,6 +1487,18 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl return isset($this->options[$option]); } + function getOption($option) { + return @$this->options[$option] ?: false; + } + + function setOption($option, $value) { + $this->options[$option] = $value; + } + + function clearOption($option) { + unset($this->options[$option]); + } + function countSelectFields() { $count = count($this->values) + count($this->annotations); if (isset($this->extra['select'])) @@ -2611,13 +2633,22 @@ class SqlCompiler { foreach ($queryset->extra['tables'] as $S) { $join = ' JOIN '; // Left joins require an ON () clause - if ($lastparen = strrpos($S, '(')) { - if (preg_match('/\bon\b/i', substr($S, $lastparen - 4, 4))) - $join = ' LEFT' . $join; - } + // TODO: Have a way to indicate a LEFT JOIN $sql .= $join.$S; } } + + // Add extra joins from QuerySet + if (isset($queryset->extra['joins'])) { + foreach ($queryset->extra['joins'] as $J) { + list($base, $constraints, $alias) = $J; + $join = $constraints ? ' LEFT JOIN ' : ' JOIN '; + $sql .= "{$join}{$base} $alias"; + if ($constraints instanceof Q) + $sql .= ' ON ('.$this->compileQ($constraints, $queryset->model).')'; + } + } + return $sql; } @@ -2965,6 +2996,7 @@ class MySqlCompiler extends SqlCompiler { $meta = $model::getMeta(); $table = $this->quote($meta['table']).' '.$rootAlias; // Handle related tables + $need_group_by = false; if ($queryset->related) { $count = 0; $fieldMap = $theseFields = array(); @@ -3010,13 +3042,16 @@ class MySqlCompiler extends SqlCompiler { } // Support retrieving only a list of values rather than a model elseif ($queryset->values) { + $additional_group_by = array(); foreach ($queryset->values as $alias=>$v) { list($f) = $this->getField($v, $model); $unaliased = $f; if ($f instanceof SqlFunction) { $fields[$f->toSql($this, $model, $alias)] = true; if ($f instanceof SqlAggregate) { - // Don't group_by aggregate expressions + // Don't group_by aggregate expressions, but if there is an + // aggergate expression, then we need a GROUP BY clause. + $need_group_by = true; continue; } } @@ -3028,8 +3063,10 @@ class MySqlCompiler extends SqlCompiler { // If there are annotations, add in these fields to the // GROUP BY clause if ($queryset->annotations && !$queryset->distinct) - $group_by[] = $unaliased; + $additional_group_by[] = $unaliased; } + if ($need_group_by && $additional_group_by) + $group_by = array_merge($group_by, $additional_group_by); } // Simple selection from one table elseif (!$queryset->aggregated) { @@ -3050,6 +3087,8 @@ class MySqlCompiler extends SqlCompiler { foreach ($queryset->annotations as $alias=>$A) { // The root model will receive the annotations, add in the // annotation after the root model's fields + if ($A instanceof SqlAggregate) + $need_group_by = true; $T = $A->toSql($this, $model, $alias); if ($fieldMap) { array_splice($fields, count($fieldMap[0][0]), 0, array($T)); @@ -3061,7 +3100,7 @@ class MySqlCompiler extends SqlCompiler { } } // If no group by has been set yet, use the root model pk - if (!$group_by && !$queryset->aggregated && !$queryset->distinct) { + if (!$group_by && !$queryset->aggregated && !$queryset->distinct && $need_group_by) { foreach ($meta['pk'] as $pk) $group_by[] = $rootAlias .'.'. $pk; } @@ -3083,12 +3122,15 @@ class MySqlCompiler extends SqlCompiler { $group_by = $group_by ? ' GROUP BY '.implode(', ', $group_by) : ''; $joins = $this->getJoins($queryset); + if ($hint = $queryset->getOption(QuerySet::OPT_INDEX_HINT)) { + $hint = " USE INDEX ({$hint})"; + } $sql = 'SELECT '; if ($queryset->hasOption(QuerySet::OPT_MYSQL_FOUND_ROWS)) $sql .= 'SQL_CALC_FOUND_ROWS '; $sql .= implode(', ', $fields).' FROM ' - .$table.$joins.$where.$group_by.$having.$sort; + .$table.$hint.$joins.$where.$group_by.$having.$sort; // UNIONS if ($queryset->chain) { // If the main query is sorted, it will need parentheses diff --git a/include/class.pagenate.php b/include/class.pagenate.php index b20ad52a5da542faa088d21d31045a338761c313..70d1ca1262d3111ed962bc54a5233d335fdd4f2a 100644 --- a/include/class.pagenate.php +++ b/include/class.pagenate.php @@ -22,6 +22,7 @@ class PageNate { var $total; var $page; var $pages; + var $approx=false; function __construct($total,$page,$limit=20,$url='') { @@ -32,7 +33,7 @@ class PageNate { $this->setTotal($total); } - function setTotal($total) { + function setTotal($total, $approx=false) { $this->total = intval($total); $this->pages = ceil( $this->total / $this->limit ); @@ -42,6 +43,7 @@ class PageNate { if (($this->limit-1)*$this->start > $this->total) { $this->start -= $this->start % $this->limit; } + $this->approx = $approx; } function setURL($url='',$vars='') { @@ -97,8 +99,12 @@ class PageNate { } $html=__('Showing')." "; if ($this->total > 0) { - $html .= sprintf(__('%1$d - %2$d of %3$d' /* Used in pagination output */), - $start, $end, $this->total); + if ($this->approx) + $html .= sprintf(__('%1$d - %2$d of about %3$d' /* Used in pagination output */), + $start, $end, $this->total); + else + $html .= sprintf(__('%1$d - %2$d of %3$d' /* Used in pagination output */), + $start, $end, $this->total); }else{ $html .= " 0 "; } diff --git a/include/class.queue.php b/include/class.queue.php index 48f82c81f44e47f7ee8dac4a8bac2eb75c84679b..e3212c502929ddbaa93785eca9f72a9f46007617 100644 --- a/include/class.queue.php +++ b/include/class.queue.php @@ -174,10 +174,7 @@ class CustomQueue extends VerySimpleModel { */ function getForm($source=null, $searchable=null) { $fields = array(); - $validator = false; if (!isset($searchable)) { - $searchable = $this->getCurrentSearchFields($source); - $validator = true; $fields = array( ':keywords' => new TextboxField(array( 'id' => 3001, @@ -188,11 +185,17 @@ class CustomQueue extends VerySimpleModel { 'classes' => 'full-width headline', 'placeholder' => __('Keywords — Optional'), ), + 'validators' => function($self, $v) { + if (mb_str_wc($v) > 3) + $self->addError(__('Search term cannot have more than 3 keywords')); + }, )), ); + + $searchable = $this->getCurrentSearchFields($source); } - foreach ($searchable as $path=>$field) + foreach ($searchable ?: array() as $path => $field) $fields = array_merge($fields, static::getSearchField($field, $path)); $form = new AdvancedSearchForm($fields, $source); @@ -998,7 +1001,8 @@ class CustomQueue extends VerySimpleModel { } function inheritCriteria() { - return $this->flags & self::FLAG_INHERIT_CRITERIA; + return $this->flags & self::FLAG_INHERIT_CRITERIA && + $this->parent_id; } function inheritColumns() { @@ -1051,8 +1055,7 @@ class CustomQueue extends VerySimpleModel { } function isPrivate() { - return !$this->isAQueue() && !$this->isPublic() && - $this->staff_id; + return !$this->isAQueue() && $this->staff_id; } function isPublic() { @@ -1081,6 +1084,57 @@ class CustomQueue extends VerySimpleModel { $this->clearFlag(self::FLAG_DISABLED); } + function getRoughCount() { + if (($count = $this->getRoughCountAPC()) !== false) + return $count; + + $query = Ticket::objects(); + $Q = $this->getBasicQuery(); + $expr = SqlCase::N()->when(new SqlExpr(new Q($Q->constraints)), + new SqlField('ticket_id')); + $query = $query->aggregate(array( + "ticket_count" => SqlAggregate::COUNT($expr) + )); + + $row = $query->values()->one(); + return $row['ticket_count']; + } + + function getRoughCountAPC() { + if (!function_exists('apcu_store')) + return false; + + $key = "rough.counts.".SECRET_SALT; + $cached = false; + $counts = apcu_fetch($key, $cached); + if ($cached === true && isset($counts["q{$this->id}"])) + return $counts["q{$this->id}"]; + + // Fetch rough counts of all queues. That is, fetch a total of the + // counts based on the queue criteria alone. Do no consider agent + // access. This should be fast and "rought" + $queues = static::objects() + ->filter(['flags__hasbit' => CustomQueue::FLAG_PUBLIC]) + ->exclude(['flags__hasbit' => CustomQueue::FLAG_DISABLED]); + + $query = Ticket::objects(); + $prefix = ""; + + foreach ($queues as $queue) { + $Q = $queue->getBasicQuery(); + $expr = SqlCase::N()->when(new SqlExpr(new Q($Q->constraints)), + new SqlField('ticket_id')); + $query = $query->aggregate(array( + "q{$queue->id}" => SqlAggregate::COUNT($expr) + )); + } + + $counts = $query->values()->one(); + + apcu_store($key, $counts, 900); + return @$counts["q{$this->id}"]; + } + function updateExports($fields, $save=true) { if (!$fields) @@ -1176,8 +1230,7 @@ class CustomQueue extends VerySimpleModel { // Set basic queue information $this->path = $this->buildPath(); - $this->setFlag(self::FLAG_INHERIT_CRITERIA, - $this->parent_id > 0 && isset($vars['inherit'])); + $this->setFlag(self::FLAG_INHERIT_CRITERIA, $this->parent_id); $this->setFlag(self::FLAG_INHERIT_COLUMNS, isset($vars['inherit-columns'])); $this->setFlag(self::FLAG_INHERIT_EXPORT, @@ -1298,6 +1351,8 @@ class CustomQueue extends VerySimpleModel { if ($this->dirty) $this->updated = SqlFunction::NOW(); + + $clearCounts = ($this->dirty || $this->__new__); if (!($rv = parent::save($refetch || $this->dirty))) return $rv; @@ -1316,6 +1371,11 @@ class CustomQueue extends VerySimpleModel { }; $move_children($this); } + + // Refetch the queue counts + if ($clearCounts) + SavedQueue::clearCounts(); + return $this->columns->saveAll() && $this->exports->saveAll() && $this->sorts->saveAll(); @@ -1336,23 +1396,35 @@ class CustomQueue extends VerySimpleModel { * visible queues. * $pid - <int> parent_id of root queue. Default is zero (top-level) */ - static function getHierarchicalQueues(Staff $staff, $pid=0) { - $all = static::objects() + static function getHierarchicalQueues(Staff $staff, $pid=0, + $primary=true) { + $query = static::objects() + ->annotate(array('_sort' => SqlCase::N() + ->when(array('sort' => 0), 999) + ->otherwise(new SqlField('sort')))) ->filter(Q::any(array( 'flags__hasbit' => self::FLAG_PUBLIC, 'flags__hasbit' => static::FLAG_QUEUE, 'staff_id' => $staff->getId(), ))) ->exclude(['flags__hasbit' => self::FLAG_DISABLED]) - ->asArray(); - + ->order_by('parent_id', '_sort', 'title'); + $all = $query->asArray(); // Find all the queues with a given parent - $for_parent = function($pid) use ($all, &$for_parent) { + $for_parent = function($pid) use ($primary, $all, &$for_parent) { $results = []; foreach (new \ArrayIterator($all) as $q) { - if ($q->parent_id == $pid) - $results[] = [ $q, $for_parent($q->getId()) ]; + if ($q->parent_id != $pid) + continue; + + if ($pid == 0 && ( + ($primary && !$q->isAQueue()) + || (!$primary && $q->isAQueue()))) + continue; + + $results[] = [ $q, $for_parent($q->getId()) ]; } + return $results; }; @@ -1392,8 +1464,10 @@ class CustomQueue extends VerySimpleModel { $queue = new static($vars); $queue->created = SqlFunction::NOW(); - if (!isset($vars['flags'])) + if (!isset($vars['flags'])) { + $queue->setFlag(self::FLAG_PUBLIC); $queue->setFlag(self::FLAG_QUEUE); + } return $queue; } @@ -2645,6 +2719,7 @@ extends VerySimpleModel { ); var $_columns; + var $_extra; function getRoot($hint=false) { switch ($hint ?: $this->root) { @@ -2662,6 +2737,12 @@ extends VerySimpleModel { return $this->id; } + function getExtra() { + if (isset($this->extra) && !isset($this->_extra)) + $this->_extra = JsonDataParser::decode($this->extra); + return $this->_extra; + } + function applySort(QuerySet $query, $reverse=false, $root=false) { $fields = CustomQueue::getSearchableFields($this->getRoot($root)); foreach ($this->getColumnPaths() as $path=>$descending) { @@ -2672,6 +2753,10 @@ extends VerySimpleModel { CustomQueue::getOrmPath($path, $query)); } } + // Add index hint if defined + if (($extra = $this->getExtra()) && isset($extra['index'])) { + $query->setOption(QuerySet::OPT_INDEX_HINT, $extra['index']); + } return $query; } @@ -2705,6 +2790,11 @@ extends VerySimpleModel { array('id' => $this->id)); } + function getAdvancedConfigForm($source=false) { + return new QueueSortAdvancedConfigForm($source ?: $this->getExtra(), + array('id' => $this->id)); + } + static function forQueue(CustomQueue $queue) { return static::objects()->filter([ 'root' => $queue->root ?: 'T', @@ -2740,6 +2830,11 @@ extends VerySimpleModel { $this->columns = JsonDataEncoder::encode($columns); } + if ($this->getExtra() !== null) { + $extra = $this->getAdvancedConfigForm($vars)->getClean(); + $this->extra = JsonDataEncoder::encode($extra); + } + if (count($errors)) return false; @@ -2999,3 +3094,24 @@ extends AbstractForm { ); } } + +class QueueSortAdvancedConfigForm +extends AbstractForm { + function getInstructions() { + return __('If unsure, leave these options blank and unset'); + } + + function buildFields() { + return array( + 'index' => new TextboxField(array( + 'label' => __('Database Index'), + 'hint' => __('Use this index when sorting on this column'), + 'required' => false, + 'layout' => new GridFluidCell(12), + 'configuration' => array( + 'placeholder' => __('Automatic'), + ), + )), + ); + } +} diff --git a/include/class.search.php b/include/class.search.php index c32916504b3d4200ccdde403e00ebe4656c4a9cb..3ffae21424c16361e8f21370a8cc4dcf829f35bb 100644 --- a/include/class.search.php +++ b/include/class.search.php @@ -384,9 +384,11 @@ class MysqlSearchBackend extends SearchBackend { $criteria->extra(array( 'tables' => array( str_replace(array(':', '{}'), array(TABLE_PREFIX, $search), - "(SELECT COALESCE(Z3.`object_id`, Z5.`ticket_id`, Z8.`ticket_id`) as `ticket_id`, SUM({}) AS `relevance` FROM `:_search` Z1 LEFT JOIN `:thread_entry` Z2 ON (Z1.`object_type` = 'H' AND Z1.`object_id` = Z2.`id`) LEFT JOIN `:thread` Z3 ON (Z2.`thread_id` = Z3.`id` AND Z3.`object_type` = 'T') LEFT JOIN `:ticket` Z5 ON (Z1.`object_type` = 'T' AND Z1.`object_id` = Z5.`ticket_id`) LEFT JOIN `:user` Z6 ON (Z6.`id` = Z1.`object_id` and Z1.`object_type` = 'U') LEFT JOIN `:organization` Z7 ON (Z7.`id` = Z1.`object_id` AND Z7.`id` = Z6.`org_id` AND Z1.`object_type` = 'O') LEFT JOIN :ticket Z8 ON (Z8.`user_id` = Z6.`id`) WHERE {} GROUP BY `ticket_id`) Z1"), - ) + "(SELECT COALESCE(Z3.`object_id`, Z5.`ticket_id`, Z8.`ticket_id`) as `ticket_id`, Z1.relevance FROM (SELECT Z1.`object_id`, Z1.`object_type`, {} AS `relevance` FROM `:_search` Z1 WHERE {} ORDER BY relevance DESC) Z1 LEFT JOIN `:thread_entry` Z2 ON (Z1.`object_type` = 'H' AND Z1.`object_id` = Z2.`id`) LEFT JOIN `:thread` Z3 ON (Z2.`thread_id` = Z3.`id` AND Z3.`object_type` = 'T') LEFT JOIN `:ticket` Z5 ON (Z1.`object_type` = 'T' AND Z1.`object_id` = Z5.`ticket_id`) LEFT JOIN `:user` Z6 ON (Z6.`id` = Z1.`object_id` and Z1.`object_type` = 'U') LEFT JOIN `:organization` Z7 ON (Z7.`id` = Z1.`object_id` AND Z7.`id` = Z6.`org_id` AND Z1.`object_type` = 'O') LEFT JOIN `:ticket` Z8 ON (Z8.`user_id` = Z6.`id`)) Z1"), + ), )); + $criteria->extra(array('order_by' => array(array(new SqlCode('Z1.relevance', 'DESC'))))); + $criteria->filter(array('ticket_id'=>new SqlCode('Z1.`ticket_id`'))); break; @@ -481,7 +483,7 @@ class MysqlSearchBackend extends SearchBackend { LEFT JOIN `".TABLE_PREFIX."_search` A2 ON (A1.`id` = A2.`object_id` AND A2.`object_type`='H') WHERE A2.`object_id` IS NULL AND (A1.poster <> 'SYSTEM') AND (LENGTH(A1.`title`) + LENGTH(A1.`body`) > 0) - ORDER BY A1.`id` DESC LIMIT 500"; + LIMIT 500"; if (!($res = db_query_unbuffered($sql, $auto_create))) return false; @@ -501,7 +503,7 @@ class MysqlSearchBackend extends SearchBackend { $sql = "SELECT A1.`ticket_id` FROM `".TICKET_TABLE."` A1 LEFT JOIN `".TABLE_PREFIX."_search` A2 ON (A1.`ticket_id` = A2.`object_id` AND A2.`object_type`='T') WHERE A2.`object_id` IS NULL - ORDER BY A1.`ticket_id` DESC LIMIT 300"; + LIMIT 300"; if (!($res = db_query_unbuffered($sql, $auto_create))) return false; @@ -524,8 +526,7 @@ class MysqlSearchBackend extends SearchBackend { $sql = "SELECT A1.`id` FROM `".USER_TABLE."` A1 LEFT JOIN `".TABLE_PREFIX."_search` A2 ON (A1.`id` = A2.`object_id` AND A2.`object_type`='U') - WHERE A2.`object_id` IS NULL - ORDER BY A1.`id` DESC"; + WHERE A2.`object_id` IS NULL"; if (!($res = db_query_unbuffered($sql, $auto_create))) return false; @@ -550,8 +551,7 @@ class MysqlSearchBackend extends SearchBackend { $sql = "SELECT A1.`id` FROM `".ORGANIZATION_TABLE."` A1 LEFT JOIN `".TABLE_PREFIX."_search` A2 ON (A1.`id` = A2.`object_id` AND A2.`object_type`='O') - WHERE A2.`object_id` IS NULL - ORDER BY A1.`id` DESC"; + WHERE A2.`object_id` IS NULL"; if (!($res = db_query_unbuffered($sql, $auto_create))) return false; @@ -575,8 +575,7 @@ class MysqlSearchBackend extends SearchBackend { require_once INCLUDE_DIR . 'class.faq.php'; $sql = "SELECT A1.`faq_id` FROM `".FAQ_TABLE."` A1 LEFT JOIN `".TABLE_PREFIX."_search` A2 ON (A1.`faq_id` = A2.`object_id` AND A2.`object_type`='K') - WHERE A2.`object_id` IS NULL - ORDER BY A1.`faq_id` DESC"; + WHERE A2.`object_id` IS NULL"; if (!($res = db_query_unbuffered($sql, $auto_create))) return false; @@ -701,18 +700,19 @@ class SavedQueue extends CustomQueue { return $this->_columns; } + static function getHierarchicalQueues(Staff $staff) { + return CustomQueue::getHierarchicalQueues($staff, 0, false); + } + /** * Fetch an AdvancedSearchForm instance for use in displaying or * configuring this search in the user interface. * */ function getForm($source=null, $searchable=array()) { - global $thisstaff; - - if (!$this->isAQueue()) - $searchable = $this->getCurrentSearchFields($source, - parent::getCriteria()); - else // Only allow supplemental matches. + $searchable = null; + if ($this->isAQueue()) + // Only allow supplemental matches. $searchable = array_intersect_key($this->getCurrentSearchFields($source), $this->getSupplementalMatches()); @@ -847,19 +847,49 @@ class SavedQueue extends CustomQueue { return (!$errors); } - static function ticketsCount($agent, $criteria=array(), - $prefix='') { + function getCount($agent, $cached=true) { + $criteria = $cached ? array() : array('id' => $this->getId()); + $counts = self::counts($agent, $criteria, $cached); + return $counts["q{$this->getId()}"] ?: 0; + } + + // Get ticket counts for queues the agent has acces to. + static function counts($agent, $criteria=array(), $cached=true) { if (!$agent instanceof Staff) return array(); - $queues = SavedQueue::objects() + // Cache TLS in seconds + $ttl = 3600; + // Cache key based on agent and salt of the installation + $key = "counts.queues.{$agent->getId()}.".SECRET_SALT; + if ($criteria && is_array($criteria)) // Consider additional criteria. + $key .= '.'.md5(serialize($criteria)); + + // only consider cache if requesed + if ($cached) { + if (function_exists('apcu_store')) { + $found = false; + $counts = apcu_fetch($key, $found); + if ($found === true) + return $counts; + } elseif (isset($_SESSION[$key]) + && isset($_SESSION[$key]['qcount']) + && (time() - $_SESSION[$key]['time']) < $ttl) { + return $_SESSION[$key]['qcount']; + } else { + // Auto clear missed session cache (if any) + unset($_SESSION[$key]); + } + } + + $queues = static::objects() ->filter(Q::any(array( - 'flags__hasbit' => CustomQueue::FLAG_PUBLIC, + 'flags__hasbit' => CustomQueue::FLAG_QUEUE, 'staff_id' => $agent->getId(), ))); - if ($criteria) + if ($criteria && is_array($criteria)) $queues->filter($criteria); $query = Ticket::objects(); @@ -870,11 +900,43 @@ class SavedQueue extends CustomQueue { $Q = $queue->getBasicQuery(); $expr = SqlCase::N()->when(new SqlExpr(new Q($Q->constraints)), new SqlField('ticket_id')); $query->aggregate(array( - "$prefix{$queue->id}" => SqlAggregate::COUNT($expr, true) + "q{$queue->id}" => SqlAggregate::COUNT($expr, true) )); + + // Add extra tables joins (if any) + if ($Q->extra && isset($Q->extra['tables'])) { + $contraints = array(); + if ($Q->constraints) + $constraints = new Q($Q->constraints); + foreach ($Q->extra['tables'] as $T) + $query->addExtraJoin(array($T, $constraints, '')); + } } - return $query->values()->one(); + $counts = $query->values()->one(); + // Always cache the results + if (function_exists('apcu_store')) { + apcu_store($key, $counts, $ttl); + } else { + // Poor man's cache + $_SESSION[$key]['qcount'] = $counts; + $_SESSION[$key]['time'] = time(); + } + + return $counts; + } + + static function clearCounts() { + if (function_exists('apcu_store')) { + if (class_exists('APCUIterator')) { + $regex = '/^counts.queues.\d+.' . preg_quote(SECRET_SALT, '/') . '$/'; + foreach (new APCUIterator($regex, APC_ITER_KEY) as $key) { + apcu_delete($key); + } + } + // Also clear rough counts + apcu_delete("rough.counts.".SECRET_SALT); + } } static function lookup($criteria) { @@ -903,6 +965,10 @@ class SavedSearch extends SavedQueue { function isSaved() { return (!$this->__new__); } + + function getCount($agent, $cached=true) { + return 500; + } } class AdhocSearch diff --git a/include/class.thread.php b/include/class.thread.php index 481a34835aed3ce179a8c83bf8e07b3ed3b84596..3d367a902f496b69e54d4bc5fc3fd710a4ef3e0a 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -1088,8 +1088,6 @@ implements TemplateVariable { if ($info instanceof AttachmentFile) $fileId = $info->getId(); - elseif (is_array($info) && isset($info['id'])) - $fileId = $info['id']; elseif ($AF = AttachmentFile::create($info)) $fileId = $AF->getId(); elseif ($add_error) { diff --git a/include/class.ticket.php b/include/class.ticket.php index 0347004deffe2efb3a92ff50ea94d8434cf1a5d6..37c80c73c669fa1b9f59dbbebfed6eb563b3fc3d 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -3200,6 +3200,9 @@ implements RestrictedAccess, Threadable, Searchable { function save($refetch=false) { if ($this->dirty) { $this->updated = SqlFunction::NOW(); + if (isset($this->dirty['status_id'])) + // Refetch the queue counts + SavedQueue::clearCounts(); } return parent::save($this->dirty || $refetch); } @@ -4210,7 +4213,8 @@ implements RestrictedAccess, Threadable, Searchable { Punt for now */ - $sql='SELECT ticket_id FROM '.TICKET_TABLE.' T1 ' + $sql='SELECT ticket_id FROM '.TICKET_TABLE.' T1' + .' USE INDEX (status_id)' .' INNER JOIN '.TICKET_STATUS_TABLE.' status ON (status.id=T1.status_id AND status.state="open") ' .' LEFT JOIN '.SLA_TABLE.' T2 ON (T1.sla_id=T2.id AND T2.flags & 1 = 1) ' diff --git a/include/i18n/en_US/queue.yaml b/include/i18n/en_US/queue.yaml index ab2b1a4bbb3453e14c7843b81a75d65751b94d9e..34a35c28707db92ce80c80af9ed8728b1cd1ce7d 100644 --- a/include/i18n/en_US/queue.yaml +++ b/include/i18n/en_US/queue.yaml @@ -29,6 +29,7 @@ --- - id: 1 title: Open + parent_id: 0 flags: 0x03 sort: 1 root: T @@ -69,14 +70,57 @@ - sort_id: 2 - sort_id: 3 - sort_id: 4 + - sort_id: 6 + - sort_id: 7 - id: 2 + title: Open + parent_id: 1 + flags: 0x2b + root: T + sort: 1 + sort_id: 4 + config: '{"criteria":[["isanswered","nset",null]],"conditions":[]}' + columns: + - column_id: 1 + bits: 1 + sort: 1 + width: 100 + heading: Ticket + - column_id: 10 + bits: 1 + sort: 2 + width: 150 + heading: Last Updated + - column_id: 3 + bits: 1 + sort: 3 + width: 300 + heading: Subject + - column_id: 4 + bits: 1 + sort: 4 + width: 185 + heading: From + - column_id: 5 + bits: 1 + sort: 5 + width: 85 + heading: Priority + - column_id: 8 + bits: 1 + sort: 6 + width: 160 + heading: Assigned To + +- id: 3 title: Answered parent_id: 1 - flags: 0x03 + flags: 0x2b root: T sort: 2 - config: '[["isanswered","set",null]]' + sort_id: 4 + config: '{"criteria":[["isanswered","set",null]],"conditions":[]}' columns: - column_id: 1 bits: 1 @@ -87,7 +131,7 @@ bits: 1 sort: 2 width: 150 - heading: Last Update + heading: Last Updated - column_id: 3 bits: 1 sort: 3 @@ -108,13 +152,53 @@ sort: 6 width: 160 heading: Assigned To - sorts: - - sort_id: 1 - - sort_id: 2 - - sort_id: 3 - - sort_id: 4 -- id: 3 +- id: 4 + title: Overdue + parent_id: 1 + flags: 0x2b + root: T + sort: 3 + sort_id: 4 + config: '{"criteria":[["isoverdue","set",null]],"conditions":[]}' + columns: + - column_id: 1 + bits: 1 + sort: 1 + width: 100 + heading: Ticket + - column_id: 9 + bits: 1 + sort: 1 + sort: 9 + width: 150 + heading: Due Date + - column_id: 3 + bits: 1 + sort: 1 + sort: 3 + width: 300 + heading: Subject + - column_id: 4 + bits: 1 + sort: 1 + sort: 4 + width: 185 + heading: From + - column_id: 5 + bits: 1 + sort: 1 + sort: 5 + width: 85 + heading: Priority + - column_id: 8 + bits: 1 + sort: 1 + sort: 6 + width: 160 + heading: Assigned To + +- id: 5 title: My Tickets parent_id: 0 flags: 0x03 @@ -157,25 +241,27 @@ - sort_id: 2 - sort_id: 3 - sort_id: 4 + - sort_id: 6 + - sort_id: 7 -- id: 4 - title: Closed - flags: 0x03 - sort: 4 +- id: 6 + title: Assigned to Me + parent_id: 5 + flags: 0x2b root: T - sort_id: 5 - config: '[["status__state","includes",{"closed":"Closed"}]]' + sort: 1 + config: '{"criteria":[["assignee","includes",{"M":"Me"}]],"conditions":[]}' columns: - column_id: 1 bits: 1 sort: 1 width: 100 heading: Ticket - - column_id: 7 + - column_id: 10 bits: 1 sort: 2 width: 150 - heading: Date Closed + heading: Last Update - column_id: 3 bits: 1 sort: 3 @@ -188,28 +274,29 @@ heading: From - column_id: 5 bits: 1 - sort: 1 sort: 5 width: 85 heading: Priority - - column_id: 8 + - column_id: 11 bits: 1 - sort: 1 sort: 6 width: 160 - heading: Closed By + heading: Department sorts: - - sort_id: 5 - sort_id: 1 - sort_id: 2 + - sort_id: 3 + - sort_id: 4 + - sort_id: 6 + - sort_id: 7 -- id: 5 - title: Assigned - parent_id: 1 - flags: 0x03 +- id: 7 + title: Assigned to Teams + parent_id: 5 + flags: 0x2b root: T - sort: 3 - config: '[["assignee","assigned",null]]' + sort: 2 + config: '{"criteria":[["assignee","!includes",{"M":"Me"}]],"conditions":[]}' columns: - column_id: 1 bits: 1 @@ -236,53 +323,323 @@ sort: 5 width: 85 heading: Priority + - column_id: 14 + bits: 1 + sort: 6 + width: 160 + heading: Team + sorts: + - sort_id: 1 + - sort_id: 2 + - sort_id: 3 + - sort_id: 4 + - sort_id: 6 + - sort_id: 7 + +- id: 8 + parent_id: 0 + title: Closed + flags: 0x03 + sort: 4 + root: T + sort_id: 5 + config: '{"criteria":[["status__state","includes",{"closed":"Closed"}]],"conditions":[]}' + columns: + - column_id: 1 + bits: 1 + sort: 1 + width: 100 + heading: Ticket + - column_id: 7 + bits: 1 + sort: 2 + width: 150 + heading: Date Closed + - column_id: 3 + bits: 1 + sort: 3 + width: 300 + heading: Subject + - column_id: 4 + bits: 1 + sort: 4 + width: 185 + heading: From - column_id: 8 bits: 1 + sort: 1 sort: 6 width: 160 - heading: Assigned To + heading: Closed By + sorts: + - sort_id: 5 + - sort_id: 1 + - sort_id: 2 + - sort_id: 3 + - sort_id: 4 + - sort_id: 6 + - sort_id: 7 -- id: 6 - title: Overdue - parent_id: 1 +- id: 9 + parent_id: 8 + title: Today flags: 0x2b + sort: 1 root: T - sort: 4 - sort_id: 4 - config: '[["isoverdue","set",null]]' + sort_id: 5 + config: '{"criteria":[["closed","period","td"]],"conditions":[]}' columns: - column_id: 1 bits: 1 sort: 1 width: 100 heading: Ticket - - column_id: 9 + - column_id: 7 + bits: 1 + sort: 2 + width: 150 + heading: Date Closed + - column_id: 3 + bits: 1 + sort: 3 + width: 300 + heading: Subject + - column_id: 4 + bits: 1 + sort: 4 + width: 185 + heading: From + - column_id: 8 bits: 1 sort: 1 - sort: 9 + sort: 6 + width: 160 + heading: Closed By + sorts: + - sort_id: 5 + - sort_id: 1 + - sort_id: 2 + - sort_id: 3 + - sort_id: 4 + - sort_id: 6 + - sort_id: 7 + +- id: 10 + parent_id: 8 + title: Yesterday + flags: 0x2b + sort: 2 + root: T + sort_id: 5 + config: '{"criteria":[["closed","period","yd"]],"conditions":[]}' + columns: + - column_id: 1 + bits: 1 + sort: 1 + width: 100 + heading: Ticket + - column_id: 7 + bits: 1 + sort: 2 width: 150 - heading: Due Date + heading: Date Closed - column_id: 3 + bits: 1 + sort: 3 + width: 300 + heading: Subject + - column_id: 4 + bits: 1 + sort: 4 + width: 185 + heading: From + - column_id: 8 + bits: 1 + sort: 1 + sort: 6 + width: 160 + heading: Closed By + sorts: + - sort_id: 5 + - sort_id: 1 + - sort_id: 2 + - sort_id: 3 + - sort_id: 4 + - sort_id: 6 + - sort_id: 7 + +- id: 11 + parent_id: 8 + title: This Week + flags: 0x2b + sort: 3 + root: T + sort_id: 5 + config: '{"criteria":[["closed","period","tw"]],"conditions":[]}' + columns: + - column_id: 1 bits: 1 sort: 1 + width: 100 + heading: Ticket + - column_id: 7 + bits: 1 + sort: 2 + width: 150 + heading: Date Closed + - column_id: 3 + bits: 1 sort: 3 width: 300 heading: Subject - column_id: 4 + bits: 1 + sort: 4 + width: 185 + heading: From + - column_id: 8 bits: 1 sort: 1 + sort: 6 + width: 160 + heading: Closed By + sorts: + - sort_id: 5 + - sort_id: 1 + - sort_id: 2 + - sort_id: 3 + - sort_id: 4 + - sort_id: 6 + - sort_id: 7 + +- id: 12 + parent_id: 8 + title: This Month + flags: 0x2b + sort: 4 + root: T + sort_id: 5 + config: '{"criteria":[["closed","period","tm"]],"conditions":[]}' + columns: + - column_id: 1 + bits: 1 + sort: 1 + width: 100 + heading: Ticket + - column_id: 7 + bits: 1 + sort: 2 + width: 150 + heading: Date Closed + - column_id: 3 + bits: 1 + sort: 3 + width: 300 + heading: Subject + - column_id: 4 + bits: 1 sort: 4 width: 185 heading: From - - column_id: 5 + - column_id: 8 bits: 1 sort: 1 - sort: 5 - width: 85 - heading: Priority + sort: 6 + width: 160 + heading: Closed By + sorts: + - sort_id: 5 + - sort_id: 1 + - sort_id: 2 + - sort_id: 3 + - sort_id: 4 + - sort_id: 6 + - sort_id: 7 + +- id: 13 + parent_id: 8 + title: This Quarter + flags: 0x2b + sort: 5 + root: T + sort_id: 6 + config: '{"criteria":[["closed","period","tq"]],"conditions":[]}' + columns: + - column_id: 1 + bits: 1 + sort: 1 + width: 100 + heading: Ticket + - column_id: 7 + bits: 1 + sort: 2 + width: 150 + heading: Date Closed + - column_id: 3 + bits: 1 + sort: 3 + width: 300 + heading: Subject + - column_id: 4 + bits: 1 + sort: 4 + width: 185 + heading: From - column_id: 8 bits: 1 sort: 1 sort: 6 width: 160 - heading: Assigned To + heading: Closed By + sorts: + - sort_id: 5 + - sort_id: 1 + - sort_id: 2 + - sort_id: 3 + - sort_id: 4 + - sort_id: 6 + - sort_id: 7 + +- id: 14 + parent_id: 8 + title: This Year + flags: 0x2b + sort: 6 + root: T + sort_id: 7 + config: '{"criteria":[["closed","period","ty"]],"conditions":[]}' + columns: + - column_id: 1 + bits: 1 + sort: 1 + width: 100 + heading: Ticket + - column_id: 7 + bits: 1 + sort: 2 + width: 150 + heading: Date Closed + - column_id: 3 + bits: 1 + sort: 3 + width: 300 + heading: Subject + - column_id: 4 + bits: 1 + sort: 4 + width: 185 + heading: From + - column_id: 8 + bits: 1 + sort: 1 + sort: 6 + width: 160 + heading: Closed By + sorts: + - sort_id: 5 + - sort_id: 1 + - sort_id: 2 + - sort_id: 3 + - sort_id: 4 + - sort_id: 6 + - sort_id: 7 diff --git a/include/i18n/en_US/queue_column.yaml b/include/i18n/en_US/queue_column.yaml index 6f7419a4af0e0074293b957b07f813668d1889e4..03250da19a83b3118bbd04eb4792218c232928dd 100644 --- a/include/i18n/en_US/queue_column.yaml +++ b/include/i18n/en_US/queue_column.yaml @@ -129,3 +129,10 @@ truncate: "wrap" annotations: "[]" conditions: "[]" + +- id: 14 + name: "Team" + primary: "team_id" + truncate: "wrap" + annotations: "[]" + conditions: "[]" diff --git a/include/i18n/en_US/queue_sort.yaml b/include/i18n/en_US/queue_sort.yaml index 09b0fb87f6530db783274f2716be91ff4f1cc8d3..35cd47d4a2dd1b12b3ff6af4931a733ba9172eea 100644 --- a/include/i18n/en_US/queue_sort.yaml +++ b/include/i18n/en_US/queue_sort.yaml @@ -27,3 +27,7 @@ - id: 6 name: Create Date columns: '["-created"]' + +- id: 7 + name: Update Date + columns: '["-lastupdate"]' diff --git a/include/staff/queue.inc.php b/include/staff/queue.inc.php index b936699df859013ba8706f9c27daba2c310a840e..fa9862bb830d4997296aad36fb3fbbbe2bc154b2 100644 --- a/include/staff/queue.inc.php +++ b/include/staff/queue.inc.php @@ -4,10 +4,11 @@ if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied'); $info = $qs = array(); - +$parent = null; if (!$queue) { $queue = CustomQueue::create(array( 'flags' => CustomQueue::FLAG_QUEUE, + 'parent_id' => 0, )); } if ($queue->__new__) { @@ -16,6 +17,7 @@ if ($queue->__new__) { $submit_text=__('Create'); } else { + $parent = $queue->parent; $title=__('Manage Custom Queue'); $action='update'; $submit_text=__('Save Changes'); @@ -29,8 +31,6 @@ else { <input type="hidden" name="do" value="<?php echo $action; ?>"> <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>"> <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> - <input type="hidden" name="root" value="<?php echo Format::htmlchars($_REQUEST['t']); ?>"> - <h2><a href="settings.php?t=tickets#queues"><?php echo __('Ticket Queues'); ?></a> <i class="icon-caret-right" style="color:rgba(0,0,0,.3);"></i> <?php echo $title; ?> <?php if (isset($queue->id)) { ?><small> @@ -63,20 +63,33 @@ else { <br/> <div class="error"><?php echo $errors['queue-name']; ?></div> <br/> + <div> + <div><strong><?php echo __("Parent Queue"); ?>:</strong></div> + <select name="parent_id" id="parent-id"> + <option value="0">— <?php echo __('Top-Level Queue'); ?> —</option> + <?php foreach (CustomQueue::queues() as $cq) { + // Queue cannot be a descendent of itself + if ($cq->id == $queue->id) + continue; + if (strpos($cq->path, "/{$queue->id}/") !== false) + continue; + ?> + <option value="<?php echo $cq->id; ?>" + <?php if ($cq->getId() == $queue->parent_id) echo 'selected="selected"'; ?> + ><?php echo $cq->getFullName(); ?></option> + <?php } ?> + </select> + <span class="error"><?php echo Format::htmlchars($errors['parent_id']); ?></span> + </div> + <div class="faded <?php echo $parent ? ' ': 'hidden'; ?>" + id="inherited-parent" style="margin-top: 1em;"> + <div><strong><i class="icon-caret-down"></i> <?php echo __('Inherited Criteria'); ?></strong></div> + <div id="parent-criteria"> + <?php echo $parent ? nl2br(Format::htmlchars($parent->describeCriteria())) : ''; ?> + </div> + </div> + <hr/> <div><strong><?php echo __("Queue Search Criteria"); ?></strong></div> - <label class="checkbox" style="line-height:1.3em"> - <input type="checkbox" class="checkbox" name="inherit" <?php - if ($queue->inheritCriteria()) echo 'checked="checked"'; - ?>/> - <?php echo __('Include parent search criteria'); - if ($queue->parent) { ?> - <span id="parent_q_crit" class="faded"> - <i class="icon-caret-right"></i> - <br/><?php - echo nl2br(Format::htmlchars($queue->parent->describeCriteria())); - ?></span> -<?php } ?> - </label> <hr/> <div class="error"><?php echo $errors['criteria']; ?></div> <div class="advanced-search"> @@ -89,27 +102,6 @@ else { </div> </td> <td style="width:35%; padding-left:40px; vertical-align:top"> - <div><strong><?php echo __("Parent Queue"); ?>:</strong></div> - <select name="parent_id" onchange="javascript: - $('#parent_q_crit').toggle($(this).find(':selected').val() - == <?php echo $queue->parent_id ?: 0; ?>);"> - <option value="0">— <?php echo __('Top-Level Queue'); ?> —</option> -<?php foreach (CustomQueue::queues() as $cq) { - // Queue cannot be a descendent of itself - if ($cq->id == $queue->id) - continue; - if (strpos($cq->path, "/{$queue->id}/") !== false) - continue; -?> - <option value="<?php echo $cq->id; ?>" - <?php if ($cq->getId() == $queue->parent_id) echo 'selected="selected"'; ?> - ><?php echo $cq->getFullName(); ?></option> -<?php } ?> - </select> - <div class="error"><?php echo Format::htmlchars($errors['parent_id']); ?></div> - - <br/> - <br/> <div><strong><?php echo __("Quick Filter"); ?></strong></div> <hr/> <select name="filter"> @@ -309,6 +301,27 @@ var Q = setInterval(function() { ); } ?> }, 25); +$('select#parent-id').change(function() { + var form = $(this).closest('form'); + var qid = parseInt($(this).val(), 10) || 0; + + if (qid > 0) { + $.ajax({ + type: "GET", + url: 'ajax.php/queue/'+qid, + dataType: 'json', + success: function(queue) { + $('#parent-name', form).html(queue.name); + $('#parent-criteria', form).html(queue.criteria); + $('#inherited-parent', form).fadeIn(); + } + }) + .done(function() { }) + .fail(function() { }); + } else { + $('#inherited-parent', form).fadeOut(); + } +}); }(); </script> </table> diff --git a/include/staff/templates/advanced-search-criteria.tmpl.php b/include/staff/templates/advanced-search-criteria.tmpl.php index 9348cf79be9a81e3cb762f1bcaf96604acedf437..309ce63cd117687c0a530cfe6935609c38e05729 100644 --- a/include/staff/templates/advanced-search-criteria.tmpl.php +++ b/include/staff/templates/advanced-search-criteria.tmpl.php @@ -11,7 +11,7 @@ if (($search instanceof SavedQueue) && !$search->checkOwnership($thisstaff)) { echo '<div class="faded">'. nl2br(Format::htmlchars($search->describeCriteria())). '</div><br>'; // Show any supplemental filters - if ($matches && count($info)) { + if ($matches) { ?> <div id="ticket-flags" style="padding:5px; border-top: 1px dotted #777;"> diff --git a/include/staff/templates/queue-navigation.tmpl.php b/include/staff/templates/queue-navigation.tmpl.php index d1061c8d43da152d5686b428fe6ee0b50943f8f1..380e03af961d5f8f6901d9bf63eadb55bb7679a4 100644 --- a/include/staff/templates/queue-navigation.tmpl.php +++ b/include/staff/templates/queue-navigation.tmpl.php @@ -15,6 +15,9 @@ $selected = (!isset($_REQUEST['a']) && $_REQUEST['queue'] == $this_queue->getId <div class="customQ-dropdown"> <ul class="scroll-height"> <!-- Add top-level queue (with count) --> + + <?php + if (!$children) { ?> <li class="top-level"> <span class="pull-right newItemQ queue-count" data-queue-id="<?php echo $q->id; ?>"><span class="faded-more">-</span> @@ -25,9 +28,9 @@ $selected = (!isset($_REQUEST['a']) && $_REQUEST['queue'] == $this_queue->getId <?php echo Format::htmlchars($q->getName()); ?> </a> - </h4> </li> - + <?php + } ?> <!-- Start Dropdown and child queues --> <?php foreach ($childs as $_) { list($q, $children) = $_; diff --git a/include/staff/templates/queue-savedsearches-nav.tmpl.php b/include/staff/templates/queue-savedsearches-nav.tmpl.php index ef06cf06c91db2b6e5a32ea0395885cc8d05cd25..34a8aff168cb10326f1e8a927f506565d3de13fb 100644 --- a/include/staff/templates/queue-savedsearches-nav.tmpl.php +++ b/include/staff/templates/queue-savedsearches-nav.tmpl.php @@ -4,6 +4,10 @@ // $searches = All visibile saved searches // $child_selected - <bool> true if the selected queue is a descendent // $adhoc - not FALSE if an adhoc advanced search exists + +$searches = SavedQueue::getHierarchicalQueues($thisstaff); +if ($queue && !$queue->parent_id && $queue->staff_id) + $child_selected = true; ?> <li class="primary-only item <?php if ($child_selected) echo 'active'; ?>"> <?php @@ -16,14 +20,9 @@ <div class="customQ-dropdown"> <ul class="scroll-height"> <!-- Start Dropdown and child queues --> - <?php foreach ($searches->findAll(array( - 'staff_id' => $thisstaff->getId(), - 'parent_id' => 0, - Q::not(array( - 'flags__hasbit' => CustomQueue::FLAG_PUBLIC - )) - )) as $q) { - if ($q->checkAccess($thisstaff)) + <?php foreach ($searches as $search) { + list($q, $children) = $search; + if ($q->checkAccess($thisstaff)) include 'queue-subnavigation.tmpl.php'; } ?> <?php diff --git a/include/staff/templates/queue-sorting-edit.tmpl.php b/include/staff/templates/queue-sorting-edit.tmpl.php index 0a2d98b0246433b59ea0f9cc1d575deb3a4ed41b..a001201a2beb94e6c3391f2965e6353882dfc8c3 100644 --- a/include/staff/templates/queue-sorting-edit.tmpl.php +++ b/include/staff/templates/queue-sorting-edit.tmpl.php @@ -5,6 +5,7 @@ * $column - <QueueColumn> instance for this column */ $sortid = $sort->getId(); +$advanced = in_array('extra', $sort::getMeta()->getFieldNames()); ?> <h3 class="drag-handle"><?php echo __('Manage Sort Options'); ?> — <?php echo $sort->get('name') ?></h3> @@ -14,10 +15,30 @@ $sortid = $sort->getId(); <form method="post" action="#tickets/search/sort/edit/<?php echo $sortid; ?>"> +<?php if ($advanced) { ?> + <ul class="clean tabs"> + <li class="active"><a href="#fields"><i class="icon-columns"></i> + <?php echo __('Fields'); ?></a></li> + <li><a href="#advanced"><i class="icon-cog"></i> + <?php echo __('Advanced'); ?></a></li> + </ul> + + <div class="tab_content" id="fields"> +<?php } ?> + <?php include 'queue-sorting.tmpl.php'; ?> +<?php if ($advanced) { ?> + </div> + + <div class="hidden tab_content" id="advanced"> + <?php echo $sort->getAdvancedConfigForm()->asTable(); ?> + </div> + +<?php } ?> + <hr> <p class="full-width"> <span class="buttons pull-left"> diff --git a/include/staff/templates/queue-tickets.tmpl.php b/include/staff/templates/queue-tickets.tmpl.php index 37d7cced74789b85fd6aa1f6cf6b0dc720eacc30..fe82ff9a21460eda9090a000f4fda147a99bc323 100644 --- a/include/staff/templates/queue-tickets.tmpl.php +++ b/include/staff/templates/queue-tickets.tmpl.php @@ -76,8 +76,20 @@ if (!$sorted && isset($sort['queuesort'])) { $page = ($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1; $pageNav = new Pagenate(PHP_INT_MAX, $page, PAGE_LIMIT); $tickets = $pageNav->paginateSimple($tickets); -$count = $tickets->total(); -$pageNav->setTotal($count); + +// Creative twist here. Create a new query copying the query criteria, sort, limit, +// and offset. Then join this new query to the $tickets query and clear the +// criteria, sort, limit, and offset from the outer query. +$criteria = clone $tickets; +$criteria->annotations = $criteria->related = $criteria->aggregated = []; +$tickets->constraints = $tickets->extra = []; +$tickets = $tickets->filter(['ticket_id__in' => $criteria->values_flat('ticket_id')]) + ->limit(false)->offset(false)->order_by(false); +# Index hint should be used on the $criteria query only +$tickets->clearOption(QuerySet::OPT_INDEX_HINT); + +$count = $queue->getCount($thisstaff); +$pageNav->setTotal($count, true); $pageNav->setURL('tickets.php', $args); ?> diff --git a/scp/autocron.php b/scp/autocron.php index 170ab3a8b421286bdfe1db6a648a13e7b971946d..cc124455f3a246ef97f37ce3fe7482d8d9797e93 100644 --- a/scp/autocron.php +++ b/scp/autocron.php @@ -45,6 +45,11 @@ if ($sec < 180 || !$ost || $ost->isUpgradePending()) require_once(INCLUDE_DIR.'class.cron.php'); +// Run tickets count every 3rd run or so... force new count by skipping cached +// results +if ((mt_rand(1, 12) % 3) == 0) + SavedQueue::counts($thisstaff, array(), false); + // Clear staff obj to avoid false credit internal notes & auto-assignment $thisstaff = null; diff --git a/scp/css/scp.css b/scp/css/scp.css index 878333317fb1d3e17f90f6d87dab46254a3b00f9..f255ae506c045beacf0499e528f31b4b5adee4b1 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -3525,6 +3525,12 @@ table.grid.form caption { margin-bottom: 5px; } +.grid.form .field > .field-hint-text { + font-style: italic; + margin: 0 10px 5px 10px; + opacity: 0.8; +} + #basic_search { background-color: #f4f4f4; margin: -10px 0; diff --git a/scp/js/scp.js b/scp/js/scp.js index c83673cd3ce944df8139dcef8f3561e7f6707b0c..2b346edcb469f9df5789ad3b591e6935b79bf473 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -1120,7 +1120,7 @@ if ($.support.pjax) { if (!$this.hasClass('no-pjax') && !$this.closest('.no-pjax').length && $this.attr('href').charAt(0) != '#') - $.pjax.click(event, {container: $this.data('pjaxContainer') || $('#pjax-container'), timeout: 2000}); + $.pjax.click(event, {container: $this.data('pjaxContainer') || $('#pjax-container'), timeout: 30000}); }) } diff --git a/scp/queues.php b/scp/queues.php index f02f4efc8a5c3faa6d047d57d9962fa5fd0e86db..997fefa2678cae361a9654c6efecfaccf0199fe9 100644 --- a/scp/queues.php +++ b/scp/queues.php @@ -44,7 +44,7 @@ if ($_POST) { $queue = CustomQueue::create(array( 'staff_id' => 0, 'title' => $_POST['queue-name'], - 'root' => $_POST['root'] ?: 'T' + 'root' => 'T' )); if ($queue->update($_POST, $errors) && $queue->save(true)) { diff --git a/scp/tickets.php b/scp/tickets.php index d100f38d7b1af8e1505301922754e5925bc264dc..86fec81f28395a49905bcd3a342bfaf159be5d15 100644 --- a/scp/tickets.php +++ b/scp/tickets.php @@ -79,16 +79,20 @@ if (!$ticket) { elseif (isset($_GET['a']) && $_GET['a'] === 'search' && ($_GET['query']) ) { - $key = substr(md5($_GET['query']), -10); - if ($_GET['search-type'] == 'typeahead') { - // Use a faster index - $criteria = ['user__emails__address', 'equal', $_GET['query']]; - } - else { - $criteria = [':keywords', null, $_GET['query']]; + $wc = mb_str_wc($_GET['query']); + if ($wc < 4) { + $key = substr(md5($_GET['query']), -10); + if ($_GET['search-type'] == 'typeahead') { + // Use a faster index + $criteria = ['user__emails__address', 'equal', $_GET['query']]; + } else { + $criteria = [':keywords', null, $_GET['query']]; + } + $_SESSION['advsearch'][$key] = [$criteria]; + $queue_id = "adhoc,{$key}"; + } else { + $errors['err'] = __('Search term cannot have more than 3 keywords'); } - $_SESSION['advsearch'][$key] = [$criteria]; - $queue_id = "adhoc,{$key}"; } $queue_key = sprintf('::Q:%s', ObjectModel::OBJECT_TYPE_TICKET); @@ -462,7 +466,7 @@ foreach ($queues as $_) { || false !== strpos($queue->getPath(), "/{$q->getId()}/")); include STAFFINC_DIR . 'templates/queue-navigation.tmpl.php'; - return ($child_selected || $selected); + return $child_selected; }); } @@ -473,10 +477,7 @@ $nav->addSubMenu(function() use ($queue) { // A queue is selected if it is the one being displayed. It is // "child" selected if its ID is in the path of the one selected $child_selected = $queue instanceof SavedSearch; - $searches = SavedSearch::forStaff($thisstaff)->getIterator(); - include STAFFINC_DIR . 'templates/queue-savedsearches-nav.tmpl.php'; - return ($child_selected || $selected); });