diff --git a/bootstrap.php b/bootstrap.php index 794613840f288b9f3dde80176777593f5c4243c2..4b64227a839c1e9cd864db6cc74a1930e9906f98 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -139,6 +139,7 @@ class Bootstrap { define('QUEUE_SORT_TABLE', $prefix.'queue_sort'); define('QUEUE_SORTING_TABLE', $prefix.'queue_sorts'); define('QUEUE_EXPORT_TABLE', $prefix.'queue_export'); + define('QUEUE_CONFIG_TABLE', $prefix.'queue_config'); define('API_KEY_TABLE',$prefix.'api_key'); define('TIMEZONE_TABLE',$prefix.'timezone'); diff --git a/include/ajax.search.php b/include/ajax.search.php index 80ebd621ca61503df97026c1f196d8c7e199ba87..d8ce8dbab1b4ef7443b5a3b664cdac63f3ce4425 100644 --- a/include/ajax.search.php +++ b/include/ajax.search.php @@ -28,8 +28,9 @@ class SearchAjaxAPI extends AjaxController { if (!$thisstaff) Http::response(403, 'Agent login required'); - $search = new SavedSearch(array( + $search = new AdhocSearch(array( 'root' => 'T', + 'staff_id' => $thisstaff->getId(), 'parent_id' => @$_GET['parent_id'] ?: 0, )); if ($search->parent_id) { @@ -39,7 +40,7 @@ class SearchAjaxAPI extends AjaxController { if (isset($_SESSION[$context]) && $key && $_SESSION[$context][$key]) $search->config = $_SESSION[$context][$key]; - $this->_tryAgain($search, $search->getForm()); + $this->_tryAgain($search); } function editSearch($id) { @@ -51,7 +52,7 @@ class SearchAjaxAPI extends AjaxController { elseif (!$search || !$search->checkAccess($thisstaff)) Http::response(404, 'No such saved search'); - $this->_tryAgain($search, $search->getForm()); + $this->_tryAgain($search); } function addField($name) { @@ -60,7 +61,9 @@ class SearchAjaxAPI extends AjaxController { if (!$thisstaff) Http::response(403, 'Agent login required'); - $search = new SavedSearch(array('root'=>'T')); + $search = new SavedSearch(array( + 'root'=>'T' + )); $searchable = $search->getSupportedMatches(); if (!($F = $searchable[$name])) Http::response(404, 'No such field: ', print_r($name, true)); @@ -82,7 +85,15 @@ class SearchAjaxAPI extends AjaxController { } function doSearch() { - $search = new SavedSearch(array('root' => 'T')); + global $thisstaff; + + if (!$thisstaff) + Http::response(403, 'Agent login is required'); + + $search = new AdhocSearch(array( + 'root' => 'T', + 'staff_id' => $thisstaff->getId())); + $form = $search->getForm($_POST); if (false === $this->_setupSearch($search, $form)) { return; @@ -139,11 +150,28 @@ class SearchAjaxAPI extends AjaxController { -$size); } - function _tryAgain($search, $form, $errors=array(), $info=array()) { - $matches = $search->getSupportedMatches(); + function _tryAgain($search, $form=null, $errors=array(), $info=array()) { + if (!$form) + $form = $search->getForm(); include STAFFINC_DIR . 'templates/advanced-search.tmpl.php'; } + function createSearch() { + global $thisstaff; + + if (!$thisstaff) + Http::response(403, 'Agent login is required'); + + + $search = SavedSearch::create(array( + 'title' => __('Add Queue'), + 'root' => 'T', + 'staff_id' => $thisstaff->getId(), + 'parent_id' => $_GET['pid'], + )); + $this->_tryAgain($search); + } + function saveSearch($id=0) { global $thisstaff; @@ -151,15 +179,16 @@ class SearchAjaxAPI extends AjaxController { Http::response(403, 'Agent login is required'); if ($id) { // update - $search = SavedSearch::lookup($id); + if (!($search = SavedSearch::lookup($id)) + || !$search->checkAccess($thisstaff)) + Http::response(404, 'No such saved search'); } else { // new search - $search = SavedSearch::create(array('root' => 'T')); - $search->staff_id = $thisstaff->getId(); + $search = SavedSearch::create(array( + 'root' => 'T', + 'staff_id' => $thisstaff->getId() + )); } - if (!$search || !$search->checkAccess($thisstaff)) - Http::response(404, 'No such saved search'); - if (false === $this->_saveSearch($search)) return; @@ -169,16 +198,23 @@ class SearchAjaxAPI extends AjaxController { $id ? __('updated') : __('created'), __('successfully')), ); - - $this->_tryAgain($search, $search->getForm(), null, $info); + $this->_tryAgain($search, null, null, $info); } function _saveSearch(SavedSearch $search) { + + // Validate the form. $form = $search->getForm($_POST); + if ($this->_hasErrors($search, $form)) + return false; + $errors = array(); if (!$search->update($_POST, $errors) - || !$search->save(true) - ) { + || !$search->save(true)) { + + $form->addError(sprintf( + __('Unable to update %s. Correct error(s) below and try again.'), + __('queue'))); $this->_tryAgain($search, $form, $errors); return false; } @@ -352,43 +388,15 @@ class SearchAjaxAPI extends AjaxController { function collectQueueCounts($ids=null) { global $thisstaff; - if (!$thisstaff) { + if (!$thisstaff) Http::response(403, 'Agent login is required'); - } - - $queues = CustomQueue::objects() - ->filter(Q::any(array( - 'flags__hasbit' => CustomQueue::FLAG_PUBLIC, - 'staff_id' => $thisstaff->getId(), - ))); + $criteria = array(); if ($ids && is_array($ids)) - $queues->filter(array('id__in' => $ids)); - - $query = Ticket::objects(); - - // Visibility contraints ------------------ - // TODO: Consider SavedSearch::ignoreVisibilityConstraints() - $visibility = $thisstaff->getTicketsVisibility(); - $query->filter($visibility); - foreach ($queues as $queue) { - $Q = $queue->getBasicQuery(); - if (count($Q->extra) || $Q->isWindowed()) { - // XXX: This doesn't work - $query->annotate(array( - 'q'.$queue->id => $Q->values_flat() - ->aggregate(array('count' => SqlAggregate::COUNT('ticket_id'))) - )); - } - else { - $expr = SqlCase::N()->when(new SqlExpr(new Q($Q->constraints)), new SqlField('ticket_id')); - $query->aggregate(array( - 'q'.$queue->id => SqlAggregate::COUNT($expr, true) - )); - } - } + $criteria = array('id__in' => $ids); + $counts = SavedQueue::ticketsCount($thisstaff, $criteria, 'q'); Http::response(200, false, 'application/json'); - return $this->encode($query->values()->one()); + return $this->encode($counts); } } diff --git a/include/class.forms.php b/include/class.forms.php index cc6572a3efdac509067b256d34f5a5df7a35de8f..9ef27efb5e8385b313159ef4505edd625ffa14b0 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -1650,6 +1650,15 @@ class BooleanField extends FormField { ); } + function describeSearchMethod($method) { + + $methods = $this->get('descsearchmethods'); + if (isset($methods[$method])) + return $methods[$method]; + + return parent::describeSearchMethod($method); + } + function getSearchMethodWidgets() { return array( 'set' => null, diff --git a/include/class.queue.php b/include/class.queue.php index ed296cd358dd17a7cd3bf3abac966cfba61b7e37..189a7a84654a2bda283c60e0a49eae443ab142e8 100644 --- a/include/class.queue.php +++ b/include/class.queue.php @@ -28,6 +28,7 @@ class CustomQueue extends VerySimpleModel { ), 'columns' => array( 'reverse' => 'QueueColumnGlue.queue', + 'constrain' => array('staff_id' =>'QueueColumnGlue.staff_id'), 'broker' => 'QueueColumnListBroker', ), 'sorts' => array( @@ -111,6 +112,10 @@ class CustomQueue extends VerySimpleModel { return $this->path ?: $this->buildPath(); } + function criteriaRequired() { + return true; + } + function getCriteria($include_parent=false) { if (!isset($this->criteria)) { $old = @$this->config[0] === '{'; @@ -164,38 +169,40 @@ class CustomQueue extends VerySimpleModel { * Parameters: * $search - <array> Request parameters ($_POST) used to update the * search beyond the current configuration of the search criteria + * $searchables - search fields - default to current if not provided */ - function getForm($source=null) { - $searchable = $this->getCurrentSearchFields($source); - $fields = array( - ':keywords' => new TextboxField(array( - 'id' => 3001, - 'configuration' => array( - 'size' => 40, - 'length' => 400, - 'autofocus' => true, - 'classes' => 'full-width headline', - 'placeholder' => __('Keywords — Optional'), - ), - )), - ); - foreach ($searchable as $path=>$field) { - $fields = array_merge($fields, static::getSearchField($field, $path)); + 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, + 'configuration' => array( + 'size' => 40, + 'length' => 400, + 'autofocus' => true, + 'classes' => 'full-width headline', + 'placeholder' => __('Keywords — Optional'), + ), + )), + ); } + foreach ($searchable as $path=>$field) + $fields = array_merge($fields, static::getSearchField($field, $path)); + $form = new AdvancedSearchForm($fields, $source); - $form->addValidator(function($form) { - $selected = 0; - foreach ($form->getFields() as $F) { - if (substr($F->get('name'), -7) == '+search' && $F->getClean()) - $selected += 1; - // Consider keyword searches - elseif ($F->get('name') == ':keywords' && $F->getClean()) - $selected += 1; - } - if (!$selected) - $form->addError(__('No fields selected for searching')); - }); + + // Field selection validator + if ($this->criteriaRequired()) { + $form->addValidator(function($form) { + if (!$form->getNumFieldsSelected()) + $form->addError(__('No fields selected for searching')); + }); + } // Load state from current configuraiton if (!$source) { @@ -235,7 +242,7 @@ class CustomQueue extends VerySimpleModel { * to contain a list extra fields by ORM path, of newly added * fields not yet saved in this object's getCriteria(). */ - function getCurrentSearchFields($source=array()) { + function getCurrentSearchFields($source=array(), $criteria=array()) { static $basic = array( 'Ticket' => array( 'status__state', @@ -257,7 +264,7 @@ class CustomQueue extends VerySimpleModel { $core[$path] = $all[$path]; // Add others from current configuration - foreach ($this->getCriteria() as $C) { + foreach ($criteria ?: $this->getCriteria() as $C) { list($path) = $C; if (isset($all[$path])) $core[$path] = $all[$path]; @@ -271,6 +278,27 @@ class CustomQueue extends VerySimpleModel { return $core; } + /** + * Fetch all supported ORM fields filterable by this search object. + */ + function getSupportedFilters() { + return static::getFilterableFields($this->getRoot()); + } + + + /** + * Get get supplemental matches for public queues. + * + */ + + function getSupplementalMatches() { + return array(); + } + + function getSupplementalCriteria() { + return array(); + } + /** * Fetch all supported ORM fields searchable by this search object. The * returned list represents searchable fields, keyed by the ORM path. @@ -376,6 +404,20 @@ class CustomQueue extends VerySimpleModel { return $fields; } + /** + * Fetch all searchable fileds, for the base object which support quick filters. + */ + function getFilterableFields($object) { + $filters = array(); + foreach (static::getSearchableFields($object) as $p => $f) { + list($label, $field) = $f; + if ($field && $field->supportsQuickFilter()) + $filters[$p] = $f; + } + + return $filters; + } + /** * Fetch the FormField instances used when for configuring a searchable * field in the user interface. This is the glue between a field @@ -577,6 +619,10 @@ class CustomQueue extends VerySimpleModel { return $fields; } + function getStandardColumns() { + return $this->getColumns(); + } + function getColumns($use_template=false) { if ($this->columns_id && ($q = CustomQueue::lookup($this->columns_id)) @@ -821,6 +867,7 @@ class CustomQueue extends VerySimpleModel { * array retrieved in ::getSearchFields() */ function describeField($info, $name=false) { + $name = $name ?: $info['field']->get('label'); return $info['field']->describeSearch($info['method'], $info['value'], $name); } @@ -865,17 +912,25 @@ class CustomQueue extends VerySimpleModel { } function checkAccess(Staff $agent) { - return $agent->getId() == $this->staff_id - || $this->hasFlag(self::FLAG_PUBLIC); + return $this->isPublic() || $this->checkOwnership($agent); + } + + function checkOwnership(Staff $agent) { + + return ($agent->getId() == $this->staff_id && + !$this->isAQueue()); + } + + function isOwner(Staff $agent) { + return $agent && $this->isPrivate() && $this->checkOwnership($agent); } function ignoreVisibilityConstraints(Staff $agent) { // For saved searches (not queues), some staff can have a permission to // see all records - return ($this->isPrivate() - && $this->checkAccess($agent) - && !$this->isASubQueue() - && $agent->hasPerm(SearchBackend::PERM_EVERYTHING)); + return (!$this->isASubQueue() + && $this->isOwner($agent) + && $agent->canSearchEverything()); } function inheritCriteria() { @@ -886,6 +941,10 @@ class CustomQueue extends VerySimpleModel { return $this->hasFlag(self::FLAG_INHERIT_COLUMNS); } + function useStandardColumns() { + return !count($this->columns); + } + function inheritExport() { return ($this->hasFlag(self::FLAG_INHERIT_EXPORT) || !count($this->exports)); @@ -924,7 +983,12 @@ class CustomQueue extends VerySimpleModel { } function isPrivate() { - return !$this->isAQueue() && !$this->hasFlag(self::FLAG_PUBLIC); + return !$this->isAQueue() && !$this->isPublic() && + $this->staff_id; + } + + function isPublic() { + return $this->hasFlag(self::FLAG_PUBLIC); } protected function hasFlag($flag) { @@ -1003,18 +1067,19 @@ class CustomQueue extends VerySimpleModel { } function update($vars, &$errors=array()) { + // Set basic search information - if (!$vars['name']) - $errors['name'] = __('A title is required'); + if (!$vars['queue-name']) + $errors['queue-name'] = __('A title is required'); elseif (($q=CustomQueue::lookup(array( - 'title' => $vars['name'], + 'title' => $vars['queue-name'], 'parent_id' => $vars['parent_id'] ?: 0, 'staff_id' => $this->staff_id))) && $q->getId() != $this->id ) - $errors['name'] = __('Saved queue with same name exists'); + $errors['queue-name'] = __('Saved queue with same name exists'); - $this->title = $vars['name']; + $this->title = $vars['queue-name']; $this->parent_id = @$vars['parent_id'] ?: 0; if ($this->parent_id && !$this->parent) $errors['parent_id'] = __('Select a valid queue'); @@ -1046,7 +1111,7 @@ class CustomQueue extends VerySimpleModel { $this->setFlag(self::FLAG_INHERIT_CRITERIA, $this->parent_id > 0 && isset($vars['inherit'])); $this->setFlag(self::FLAG_INHERIT_COLUMNS, - $this->parent_id > 0 && isset($vars['inherit-columns'])); + isset($vars['inherit-columns'])); $this->setFlag(self::FLAG_INHERIT_EXPORT, $this->parent_id > 0 && isset($vars['inherit-exports'])); $this->setFlag(self::FLAG_INHERIT_SORTING, @@ -1057,37 +1122,17 @@ class CustomQueue extends VerySimpleModel { // No columns -- imply column inheritance $this->setFlag(self::FLAG_INHERIT_COLUMNS); } - if (isset($vars['columns']) && !$this->hasFlag(self::FLAG_INHERIT_COLUMNS)) { - $new = $vars['columns']; - $order = array_keys($new); - foreach ($this->columns as $col) { - $key = $col->column_id; - if (!isset($vars['columns'][$key])) { - $this->columns->remove($col); - continue; - } - $info = $vars['columns'][$key]; - $col->set('sort', array_search($key, $order)); - $col->set('heading', $info['heading']); - $col->set('width', $info['width']); - $col->setSortable($info['sortable']); - unset($new[$key]); - } - // Add new columns - foreach ($new as $info) { - $glue = new QueueColumnGlue(array( - 'column_id' => $info['column_id'], - 'sort' => array_search($info['column_id'], $order), - 'heading' => $info['heading'], - 'width' => $info['width'] ?: 100, - 'bits' => $info['sortable'] ? QueueColumn::FLAG_SORTABLE : 0, - )); - $glue->queue = $this; - $this->columns->add( - QueueColumn::lookup($info['column_id']), $glue); - } - // Re-sort the in-memory columns array - $this->columns->sort(function($c) { return $c->sort; }); + + + if ($this->getId() + && isset($vars['columns']) + && !$this->hasFlag(self::FLAG_INHERIT_COLUMNS)) { + + + if ($this->columns->update($vars['columns'], $errors, array( + 'queue_id' => $this->getId(), + 'staff_id' => $this->staff_id))) + $this->columns->reset(); } // Update export fields for the queue @@ -1237,13 +1282,10 @@ class CustomQueue extends VerySimpleModel { static function create($vars=false) { - global $thisstaff; $queue = new static($vars); $queue->created = SqlFunction::NOW(); $queue->setFlag(self::FLAG_QUEUE); - if ($thisstaff) - $queue->staff_id = $thisstaff->getId(); return $queue; } @@ -2175,6 +2217,71 @@ extends VerySimpleModel { } } + +class QueueConfig +extends VerySimpleModel { + static $meta = array( + 'table' => QUEUE_CONFIG_TABLE, + 'pk' => array('queue_id', 'staff_id'), + 'joins' => array( + 'queue' => array( + 'constraint' => array( + 'queue_id' => 'CustomQueue.id'), + ), + 'staff' => array( + 'constraint' => array( + 'staff_id' => 'Staff.staff_id', + ) + ), + 'columns' => array( + 'reverse' => 'QueueColumnGlue.config', + 'constrain' => array('staff_id' =>'QueueColumnGlue.staff_id'), + 'broker' => 'QueueColumnListBroker', + ), + ), + ); + + function getSettings() { + return JsonDataParser::decode($this->setting); + } + + + function update($vars, &$errors) { + + // settings of interest + $setting = array( + 'sort_id' => (int) $vars['sort_id'], + 'filter' => $vars['filter'], + 'inherit-columns' => isset($vars['inherit-columns']), + 'criteria' => $vars['criteria'] ?: array(), + ); + + if (!$setting['inherit-columns'] && $vars['columns']) { + if (!$this->columns->update($vars['columns'], $errors, array( + 'queue_id' => $this->queue_id, + 'staff_id' => $this->staff_id))) + $setting['inherit-columns'] = true; + $this->columns->reset(); + } + + $this->setting = JsonDataEncoder::encode($setting); + + return $this->save(); + } + + function save($refetch=false) { + if ($this->dirty) + $this->updated = SqlFunction::NOW(); + return parent::save($refetch || $this->dirty); + } + + static function create($vars=false) { + $inst = new static($vars); + return $inst; + } +} + + class QueueExport extends VerySimpleModel { static $meta = array( @@ -2212,16 +2319,23 @@ class QueueColumnGlue extends VerySimpleModel { static $meta = array( 'table' => QUEUE_COLUMN_TABLE, - 'pk' => array('queue_id', 'column_id'), + 'pk' => array('queue_id', 'staff_id', 'column_id'), 'joins' => array( 'column' => array( 'constraint' => array('column_id' => 'QueueColumn.id'), ), 'queue' => array( - 'constraint' => array('queue_id' => 'CustomQueue.id'), + 'constraint' => array( + 'queue_id' => 'CustomQueue.id', + 'staff_id' => 'CustomQueue.staff_id'), + ), + 'config' => array( + 'constraint' => array( + 'queue_id' => 'QueueConfig.queue_id', + 'staff_id' => 'QueueConfig.staff_id'), ), ), - 'select_related' => array('column', 'queue'), + 'select_related' => array('column'), 'ordering' => array('sort'), ); } @@ -2253,6 +2367,44 @@ extends InstrumentedList { parent::add($anno, false); return $anno; } + + function update($columns, &$errors, $options=array()) { + + + $new = $columns; + $order = array_keys($new); + foreach ($this as $col) { + $key = $col->column_id; + if (!isset($columns[$key])) { + $this->remove($col); + continue; + } + $info = $columns[$key]; + $col->set('sort', array_search($key, $order)); + $col->set('heading', $info['heading']); + $col->set('width', $info['width']); + $col->setSortable($info['sortable']); + unset($new[$key]); + } + // Add new columns + foreach ($new as $info) { + $glue = new QueueColumnGlue(array( + 'staff_id' => $options['staff_id'] ?: 0 , + 'queue_id' => $options['queue_id'] ?: 0, + 'column_id' => $info['column_id'], + 'sort' => array_search($info['column_id'], $order), + 'heading' => $info['heading'], + 'width' => $info['width'] ?: 100, + 'bits' => $info['sortable'] ? QueueColumn::FLAG_SORTABLE : 0, + )); + + $this->add(QueueColumn::lookup($info['column_id']), $glue); + } + // Re-sort the in-memory columns array + $this->sort(function($c) { return $c->sort; }); + + return $this->saveAll(); + } } class QueueSort diff --git a/include/class.search.php b/include/class.search.php index f1d1510de882d8c47169f0af106f01c79dfdf98c..8c5a7c945413cd5355cd525d89dffdf77f028339 100644 --- a/include/class.search.php +++ b/include/class.search.php @@ -646,14 +646,29 @@ MysqlSearchBackend::register(); // Saved search system /** - * A special case of the custom queues used to represent an advanced search. + * Custom Queue truly represent a saved advanced search. */ -class SavedSearch extends CustomQueue { +class SavedQueue extends CustomQueue { // Override the ORM relationship to force no children private $children = false; + private $_config; + private $_criteria; + private $_columns; + private $_settings; + private $_form; - function isSaved() { - return true; + + + function __onload() { + global $thisstaff; + + // Load custom settings for this staff + if ($thisstaff) { + $this->_config = QueueConfig::lookup(array( + 'queue_id' => $this->getId(), + 'staff_id' => $thisstaff->getId()) + ); + } } static function forStaff(Staff $agent) { @@ -664,14 +679,216 @@ class SavedSearch extends CustomQueue { ->exclude(array('flags__hasbit'=>self::FLAG_QUEUE)); } + private function getSettings() { + if (!isset($this->_settings)) { + $this->_settings = array(); + if ($this->_config) + $this->_settings = $this->_config->getSettings(); + } + + return $this->_settings; + } + + private function getCustomColumns() { + + if (!isset($this->_columns)) { + $this->_columns = array(); + if ($this->_config + && $this->_config->columns->count()) + $this->_columns = $this->_config->columns; + } + + return $this->_columns; + } + + /** + * 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 = array_intersect_key($this->getCurrentSearchFields($source), + $this->getSupplementalMatches()); + + return parent::getForm($source, $searchable); + } + + /** + * Get get supplemental matches for public queues. + * + */ + function getSupplementalMatches() { + // Target flags + $flags = array('isoverdue', 'isassigned', 'isreopened', 'isanswered'); + $current = array(); + // Check for closed state - whih disables above flags + foreach (parent::getCriteria() as $c) { + if (!strcasecmp($c[0], 'status__state') + && isset($c[2]['closed'])) + return array(); + + $current[] = $c[0]; + } + + // Filter out fields already in criteria + $matches = array_intersect_key($this->getSupportedMatches(), + array_flip(array_diff($flags, $current))); + + return $matches; + } + + function criteriaRequired() { + return !$this->isAQueue(); + } + + function describeCriteria($criteria=false){ + $criteria = $criteria ?: parent::getCriteria(); + return parent::describeCriteria($criteria); + } + + function getCriteria($include_parent=true) { + + if (!isset($this->_criteria)) { + $this->getSettings(); + $this->_criteria = $this->_settings['criteria'] ?: array(); + } + + $criteria = $this->_criteria; + if ($include_parent) + $criteria = array_merge($criteria, + parent::getCriteria($include_parent)); + + + return $criteria; + } + + function getSupplementalCriteria() { + return $this->getCriteria(false); + } + + function useStandardColumns() { + + $this->getSettings(); + if ($this->getCustomColumns() + && isset($this->_settings['inherit-columns'])) + return $this->_settings['inherit-columns']; + + // owner?? edit away. + if ($this->_config + && $this->_config->staff_id == $this->staff_id) + return false; + + return parent::useStandardColumns(); + } + + function getStandardColumns() { + return parent::getColumns(($this->parent)); + } + + function getColumns($use_template=false) { + + if (!$this->useStandardColumns() && ($columns=$this->getCustomColumns())) + return $columns; + + return parent::getColumns($use_template); + } + function update($vars, &$errors=array()) { - if (!parent::update($vars, $errors)) + global $thisstaff; + + if (!$this->checkAccess($thisstaff)) return false; - // Personal queues _always_ inherit from their parent - $this->setFlag(self::FLAG_INHERIT_CRITERIA, $this->parent_id > 0); + if ($this->checkOwnership($thisstaff)) { + // Owner of the queue - can update everything + if (!parent::update($vars, $errors)) + return false; + + // Personal queues _always_ inherit from their parent + $this->setFlag(self::FLAG_INHERIT_CRITERIA, $this->parent_id > + 0); - return count($errors) === 0; + return true; + } + + // Agent's config for public queue. + if (!$this->_config) + $this->_config = QueueConfig::create(array( + 'queue_id' => $this->getId(), + 'staff_id' => $thisstaff->getId())); + + // Validate & isolate supplemental criteria (if any) + $vars['criteria'] = array(); + if (isset($vars['fields'])) { + $form = $this->getForm($vars, $thisstaff); + if ($form->isValid()) { + $criteria = self::isolateCriteria($form->getClean(), + $this->getRoot()); + $allowed = $this->getSupplementalMatches(); + foreach ($criteria as $k => $c) + if (!isset($allowed[$c[0]])) + unset($criteria[$k]); + + $vars['criteria'] = $criteria ?: array(); + } else { + $errors['criteria'] = __('Validation errors exist on supplimental criteria'); + } + } + + if (!$errors && $this->_config->update($vars, $errors)) + $this->_settings = $this->_criteria = null; + + return (!$errors); + } + + static function ticketsCount($agent, $criteria=array(), + $prefix='') { + + if (!$agent instanceof Staff) + return array(); + + $queues = SavedQueue::objects() + ->filter(Q::any(array( + 'flags__hasbit' => CustomQueue::FLAG_PUBLIC, + 'staff_id' => $agent->getId(), + ))); + + if ($criteria) + $queues->filter($criteria); + + $query = Ticket::objects(); + // Apply tickets visibility for the agent + $query = $agent->applyVisibility($query); + // Aggregate constraints + foreach ($queues as $queue) { + $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) + )); + } + + return $query->values()->one(); + } + + static function lookup($criteria) { + $queue = parent::lookup($criteria); + // Annoted cusom settings (if any) + if (($c=$queue->_config)) { + $queue->_settings = $c->getSettings() ?: array(); + $queue = AnnotatedModel::wrap($queue, + array_intersect_key($queue->_settings, + array_flip(array('sort_id', 'filter')))); + $queue->_config = $c; + } + + return $queue; } static function create($vars=false) { @@ -681,6 +898,13 @@ class SavedSearch extends CustomQueue { } } +class SavedSearch extends SavedQueue { + + function isSaved() { + return (!$this->__new__); + } +} + class AdhocSearch extends SavedSearch { @@ -717,17 +941,34 @@ extends SavedSearch { } } +// AdvacedSearchForm class AdvancedSearchForm extends SimpleForm { static $id = 1337; + + function getNumFieldsSelected() { + $selected = 0; + foreach ($this->getFields() as $F) { + if (substr($F->get('name'), -7) == '+search' + && $F->getClean()) + $selected += 1; + // Consider keyword searches + elseif ($F->get('name') == ':keywords' + && $F->getClean()) + $selected += 1; + } + return $selected; + } } // Advanced search special fields class AdvancedSearchSelectionField extends ChoiceField { - function getSearchQ($method, $value, $name=false) { - + function hasIdValue() { + return false; + } + function getSearchQ($method, $value, $name=false) { switch ($method) { case 'includes': case '!includes': @@ -741,6 +982,13 @@ class AdvancedSearchSelectionField extends ChoiceField { $Q->negate(); return $Q; break; + // osTicket commonly uses `0` to represent an unset state, so + // the set and unset checks need to check for both not null and + // nonzero + case 'nset': + return new Q([$name => 0]); + case 'set': + return Q::not([$name => 0]); default: return parent::getSearchQ($method, $value, $name); } @@ -903,12 +1151,45 @@ class AssigneeChoiceField extends ChoiceField { } function applyOrderBy($query, $reverse=false, $name=false) { + global $cfg; + $reverse = $reverse ? '-' : ''; - return $query->order_by("{$reverse}staff__firstname", - "{$reverse}staff__lastname", "{$reverse}team__name"); + switch ($cfg->getAgentNameFormat()) { + case 'last': + case 'lastfirst': + case 'legal': + $query->order_by("{$reverse}staff__lastname", + "{$reverse}staff__firstname", "{$reverse}team__name"); + break; + default: + $query->order_by("{$reverse}staff__firstname", + "{$reverse}staff__lastname", "{$reverse}team__name"); + } + + return $query; } } +class AssignedField extends AssigneeChoiceField { + + function getSearchMethods() { + return array( + 'assigned' => __('assigned'), + '!assigned' => __('unassigned'), + ); + } + + function addToQuery($query, $name=false) { + return $query->values('staff_id', 'team_id'); + } + + function from_query($row, $name=false) { + return ($row['staff_id'] || $row['staff_id']) + ? __('Yes') : __('No'); + } + +} + /** * Simple trait which changes the SQL for "has a value" and "does not have a * value" to check for zero or non-zero. Useful for not nullable fields. @@ -933,7 +1214,7 @@ class AgentSelectionField extends AdvancedSearchSelectionField { use ZeroMeansUnset; function getChoices($verbose=false) { - return Staff::getStaffMembers(); + return array('M' => __('Me')) + Staff::getStaffMembers(); } function toString($value) { @@ -947,6 +1228,18 @@ class AgentSelectionField extends AdvancedSearchSelectionField { parent::toString($value); } + function getSearchQ($method, $value, $name=false) { + global $thisstaff; + // unpack me + if (isset($value['M']) && $thisstaff) { + $value[$thisstaff->getId()] = $thisstaff->getName(); + unset($value['M']); + } + + return parent::getSearchQ($method, $value, $name); + } + + function applyOrderBy($query, $reverse=false, $name=false) { global $cfg; @@ -978,11 +1271,23 @@ class DepartmentManagerSelectionField extends AgentSelectionField { } } -class TeamSelectionField extends ChoiceField { - use ZeroMeansUnset; +class TeamSelectionField extends AdvancedSearchSelectionField { function getChoices($verbose=false) { - return Team::getTeams(); + return array('T' => __('One of my teams')) + Team::getTeams(); + } + + function getSearchQ($method, $value, $name=false) { + global $thisstaff; + + // Unpack my teams + if (isset($value['T']) && $thisstaff + && ($teams = $thisstaff->getTeams())) { + unset($value['T']); + $value = $value + array_flip($teams); + } + + return parent::getSearchQ($method, $value, $name); } function applyOrderBy($query, $reverse=false, $name=false) { diff --git a/include/class.staff.php b/include/class.staff.php index d3676afb5d71a3783cb87a3332d2a30f28cfdca2..0c35a0c1c4b5fb33f9b327a811d68d4aa624e4d4 100644 --- a/include/class.staff.php +++ b/include/class.staff.php @@ -359,7 +359,7 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable { $depts = array(); if (($res=db_query($sql)) && db_num_rows($res)) { while(list($id)=db_fetch_row($res)) - $depts[] = $id; + $depts[] = (int) $id; } /* ORM method — about 2.0ms slower @@ -491,6 +491,10 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable { return false; } + function canSearchEverything() { + return $this->hasPerm(SearchBackend::PERM_EVERYTHING); + } + function canManageTickets() { return $this->hasPerm(Ticket::PERM_DELETE, false) || $this->hasPerm(Ticket::PERM_TRANSFER, false) @@ -556,7 +560,7 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable { if (!isset($this->_teams)) { $this->_teams = array(); foreach ($this->teams as $team) - $this->_teams[] = $team->team_id; + $this->_teams[] = (int) $team->team_id; } return $this->_teams; @@ -588,6 +592,10 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable { return $visibility; } + function applyVisibility($query) { + return $query->filter($this->getTicketsVisibility()); + } + /* stats */ function resetStats() { $this->stats = array(); diff --git a/include/class.ticket.php b/include/class.ticket.php index 3d7e148ca7040e29302d943d5fb3308dddc89c88..a1147c2cbb29115766bfe4279cc9d98aef31c97f 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -2084,10 +2084,14 @@ implements RestrictedAccess, Threadable, Searchable { 'label' => __('Create Date'), 'configuration' => array('fromdb' => true), )), - 'est_duedate' => new DatetimeField(array( + 'duedate' => new DatetimeField(array( 'label' => __('Due Date'), 'configuration' => array('fromdb' => true), )), + 'est_duedate' => new DatetimeField(array( + 'label' => __('SLA Due Date'), + 'configuration' => array('fromdb' => true), + )), 'reopened' => new DatetimeField(array( 'label' => __('Reopen Date'), 'configuration' => array('fromdb' => true), @@ -2120,9 +2124,20 @@ implements RestrictedAccess, Threadable, Searchable { )), 'isoverdue' => new BooleanField(array( 'label' => __('Overdue'), + 'descsearchmethods' => array( + 'set' => '%s', + 'nset' => 'Not %s' + ), )), 'isanswered' => new BooleanField(array( 'label' => __('Answered'), + 'descsearchmethods' => array( + 'set' => '%s', + 'nset' => 'Not %s' + ), + )), + 'isassigned' => new AssignedField(array( + 'label' => __('Assigned'), )), 'ip_address' => new TextboxField(array( 'label' => __('IP Address'), diff --git a/include/i18n/en_US/queue.yaml b/include/i18n/en_US/queue.yaml index 0d572c5c7d3ea24779aceaa3b6fc3ae07312ff2b..ab2b1a4bbb3453e14c7843b81a75d65751b94d9e 100644 --- a/include/i18n/en_US/queue.yaml +++ b/include/i18n/en_US/queue.yaml @@ -75,7 +75,7 @@ parent_id: 1 flags: 0x03 root: T - sort: 4 + sort: 2 config: '[["isanswered","set",null]]' columns: - column_id: 1 @@ -114,8 +114,6 @@ - sort_id: 3 - sort_id: 4 - - - id: 3 title: My Tickets parent_id: 0 @@ -205,55 +203,12 @@ - sort_id: 1 - sort_id: 2 -- title: Unassigned - parent_id: 1 - flags: 0x2b - root: T - sort: 1 - config: '[["assignee","!assigned",null]]' - columns: - - column_id: 1 - bits: 1 - sort: 1 - sort: 1 - width: 100 - heading: Ticket - - column_id: 10 - bits: 1 - sort: 1 - sort: 2 - width: 150 - heading: Last Update - - 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: 11 - bits: 1 - sort: 1 - sort: 6 - width: 160 - heading: Department - -- title: Assigned +- id: 5 + title: Assigned parent_id: 1 flags: 0x03 root: T - sort: 2 + sort: 3 config: '[["assignee","assigned",null]]' columns: - column_id: 1 @@ -287,11 +242,12 @@ width: 160 heading: Assigned To -- title: Overdue +- id: 6 + title: Overdue parent_id: 1 flags: 0x2b root: T - sort: 3 + sort: 4 sort_id: 4 config: '[["isoverdue","set",null]]' columns: @@ -300,7 +256,7 @@ sort: 1 width: 100 heading: Ticket - - column_id: 10 + - column_id: 9 bits: 1 sort: 1 sort: 9 @@ -330,18 +286,3 @@ sort: 6 width: 160 heading: Assigned To - -- title: Personal Tickets - parent_id: 3 - flags: 0x2b - root: T - sort: 1 - config: '{"criteria":[["assignee","includes",{"M":"Me"}]]}' - -- title: Teams Tickets - parent_id: 3 - flags: 0x2b - root: T - sort: 2 - config: '{"criteria":[["team_id","set",null]],"conditions":[]}' - filter: team_id diff --git a/include/i18n/en_US/queue_column.yaml b/include/i18n/en_US/queue_column.yaml index 1c1d011f5df1cef9b1cc08eb3f002c7124778113..6f7419a4af0e0074293b957b07f813668d1889e4 100644 --- a/include/i18n/en_US/queue_column.yaml +++ b/include/i18n/en_US/queue_column.yaml @@ -92,7 +92,8 @@ - id: 9 name: "Due Date" - primary: "est_duedate" + primary: "duedate" + secondary: "est_duedate" filter: "date:human" truncate: "wrap" annotations: "[]" diff --git a/include/staff/queue.inc.php b/include/staff/queue.inc.php index bf41bd1a48b42204c95fbec0b7fda987229c032a..8506fbee9d1ce3d78b429fe74c969f7f2771f158 100644 --- a/include/staff/queue.inc.php +++ b/include/staff/queue.inc.php @@ -57,11 +57,11 @@ else { <table class="table"> <td style="width:60%; vertical-align:top"> <div><strong><?php echo __('Queue Name'); ?>:</strong></div> - <input type="text" name="name" value="<?php + <input type="text" name="queue-name" value="<?php echo Format::htmlchars($queue->getName()); ?>" style="width:100%" /> - <br/> + <div class="error"><?php echo $errors['queue-name']; ?></div> <br/> <div><strong><?php echo __("Queue Search Criteria"); ?></strong></div> <label class="checkbox" style="line-height:1.3em"> @@ -120,10 +120,8 @@ else { if ($queue->parent && ($qf = $queue->parent->getQuickFilterField())) echo sprintf(' (%s)', $qf->getLabel()); ?> —</option> -<?php foreach (CustomQueue::getSearchableFields('Ticket') as $path=>$f) { +<?php foreach ($queue->getSupportedFilters() as $path => $f) { list($label, $field) = $f; - if (!$field->supportsQuickFilter()) - continue; ?> <option value="<?php echo $path; ?>" <?php if ($path == $queue->filter) echo 'selected="selected"'; ?> @@ -314,8 +312,8 @@ var Q = setInterval(function() { }(); </script> </table> - </div> - + </div> + <div class="hidden tab_content" id="preview-tab"> <div id="preview"> </div> @@ -339,7 +337,7 @@ var Q = setInterval(function() { <div class="hidden tab_content" id="conditions-tab"> <div style="margin-bottom: 15px"><?php echo __("Conditions are used to change the view of the data in a row based on some conditions of the data. For instance, a column might be shown bold if some condition is met."); - ?> <?php echo __("These conditions apply to an entire row in the queue."); + ?> <?php echo __("These conditions apply to an entire row in the queue."); ?></div> <div class="conditions"> <?php diff --git a/include/staff/templates/advanced-search-criteria.tmpl.php b/include/staff/templates/advanced-search-criteria.tmpl.php index 136c7d139bb7a35d382757fe2e534230f79d997a..9348cf79be9a81e3cb762f1bcaf96604acedf437 100644 --- a/include/staff/templates/advanced-search-criteria.tmpl.php +++ b/include/staff/templates/advanced-search-criteria.tmpl.php @@ -1,9 +1,29 @@ <?php -foreach ($form->errors(true) ?: array() as $message) { - ?><div class="error-banner"><?php echo $message;?></div><?php +// Display errors if any +foreach ($form->errors(true) ?: array() as $message) + echo sprintf('<div class="error-banner">%s</div>', + Format::htmlchars($message)); +// Current search fields. +$info = $search->getSearchFields($form) ?: array(); +if (($search instanceof SavedQueue) && !$search->checkOwnership($thisstaff)) { + $matches = $search->getSupplementalMatches(); + // Uneditable core criteria for the queue + echo '<div class="faded">'. nl2br(Format::htmlchars($search->describeCriteria())). + '</div><br>'; + // Show any supplemental filters + if ($matches && count($info)) { + ?> + <div id="ticket-flags" + style="padding:5px; border-top: 1px dotted #777;"> + <strong><i class="icon-caret-down"></i> <?php + echo __('Supplemental Filters'); ?></strong> + </div> +<?php + } +} else { + $matches = $search->getSupportedMatches(); } -$info = $search->getSearchFields($form); foreach (array_keys($info) as $F) { ?><input type="hidden" name="fields[]" value="<?php echo $F; ?>"/><?php } @@ -51,8 +71,7 @@ foreach ($form->getFields() as $name=>$field) { $this.closest('.adv-search-field-container').find('.adv-search-field-body').slideDown('fast'); $this.find('span.faded').hide(); $this.find('i').removeClass('icon-caret-right').addClass('icon-caret-down'); - return false; -"><i class="icon-caret-right"></i> + return false; "><i class="icon-caret-right"></i> <span class="faded"><?php echo $search->describeField($info[$name]); ?></span> </a> </span> @@ -62,21 +81,20 @@ foreach ($form->getFields() as $name=>$field) { } ?> </fieldset> <?php if ($name[0] == ':' && substr($name, -7) == '+search') { - list($N,) = explode('+', $name, 2); -?> + list($N,) = explode('+', $name, 2); ?> <input type="hidden" name="fields[]" value="<?php echo $N; ?>"/> <?php } } if (!$first_field) echo '</div></div>'; -?> + +if ($matches && is_array($matches)) { ?> <div id="extra-fields"></div> <hr/> <i class="icon-plus-sign"></i> <select id="search-add-new-field" name="new-field" style="max-width: 300px;"> <option value="">— <?php echo __('Add Other Field'); ?> —</option> <?php -if (is_array($matches)) { foreach ($matches as $path => $F) { # Skip fields already listed above the drop-down if (isset($already_listed[$path])) @@ -86,7 +104,7 @@ foreach ($matches as $path => $F) { if (isset($state[$path])) echo 'disabled="disabled"'; ?>><?php echo $label; ?></option> <?php } -} ?> +?> </select> <script> $(function() { @@ -100,9 +118,12 @@ $(function() { if (!json.success) return false; $(that).find(':selected').prop('disabled', true); + $(that).find('option:eq("")').prop('selected', true); $('#extra-fields').append($(json.html)); } }); }); }); </script> +<?php +} ?> diff --git a/include/staff/templates/advanced-search.tmpl.php b/include/staff/templates/advanced-search.tmpl.php index 71d2deedcb0314a1dd4355bad643fb42a68d363d..0fe2b99dcb420a7d6297e18e8c8426afbed7b727 100644 --- a/include/staff/templates/advanced-search.tmpl.php +++ b/include/staff/templates/advanced-search.tmpl.php @@ -1,11 +1,15 @@ <?php +global $thisstaff; + $parent_id = $_REQUEST['parent_id'] ?: $search->parent_id; if ($parent_id - && (!($parent = CustomQueue::lookup($parent_id))) + && is_numeric($parent_id) + && (!($parent = SavedQueue::lookup($parent_id))) ) { $parent_id = 0; } +$editable = $search->checkOwnership($thisstaff); $queues = array(); foreach (CustomQueue::queues() as $q) $queues[$q->id] = $q->getFullName(); @@ -26,10 +30,21 @@ if ($info['error']) { echo sprintf('<p id="msg_warning">%s</p>', $info['warn']); } elseif ($info['msg']) { echo sprintf('<p id="msg_notice">%s</p>', $info['msg']); -} ?> -<form action="#tickets/search" method="post" name="search" id="advsearch" +} + +// Form action +$action = '#tickets/search'; +if ($search->isSaved() && $search->getId()) + $action .= sprintf('/%s/save', $search->getId()); +elseif (!$search instanceof AdhocSearch) + $action .= '/save'; +?> +<form action="<?php echo $action; ?>" method="post" name="search" id="advsearch" class="<?php echo ($search->isSaved() || $parent) ? 'savedsearch' : 'adhocsearch'; ?>"> <input type="hidden" name="id" value="<?php echo $search->getId(); ?>"> +<?php +if ($editable) { + ?> <div class="flex row"> <div class="span12"> <select id="parent" name="parent_id" > @@ -43,10 +58,16 @@ foreach ($queues as $id => $name) { </select> </div> </div> +<?php +} ?> <ul class="clean tabs"> <li class="active"><a href="#criteria"><i class="icon-search"></i> <?php echo __('Criteria'); ?></a></li> <li><a href="#columns"><i class="icon-columns"></i> <?php echo __('Columns'); ?></a></li> - <li><a href="#fields"><i class="icon-download"></i> <?php echo __('Export'); ?></a></li> + <?php + if ($search->isSaved()) { ?> + <li><a href="#settings"><i class="icon-cog"></i> <?php echo __('Settings'); ?></a></li> + <?php + } ?> </ul> <div class="tab_content" id="criteria"> @@ -68,51 +89,80 @@ foreach ($queues as $id => $name) { </div> </div> <input type="hidden" name="a" value="search"> - <?php include STAFFINC_DIR . 'templates/advanced-search-criteria.tmpl.php'; ?> + <?php + include STAFFINC_DIR . 'templates/advanced-search-criteria.tmpl.php'; + ?> </div> </div> </div> -<div class="tab_content hidden" id="columns" style="overflow-y: auto; -height:auto;"> +<div class="tab_content hidden" id="columns"> <?php include STAFFINC_DIR . "templates/queue-columns.tmpl.php"; ?> </div> -<div class="tab_content hidden" id="fields"> +<?php +if ($search->isSaved()) { ?> +<div class="tab_content hidden" id="settings"> <?php - include STAFFINC_DIR . "templates/queue-fields.tmpl.php"; ?> + include STAFFINC_DIR . "templates/savedqueue-settings.tmpl.php"; + ?> </div> - <?php - $save = (($parent && !$search->isSaved()) || $errors); ?> +<?php +} else { // Not saved. + $save = (($parent && !$search->isSaved()) || isset($_POST['queue-name'])); +?> +<div> <div style="margin-top:10px;"><a href="#" id="save"><i class="icon-caret-<?php echo $save ? 'down' : 'right'; ?>"></i> <span><?php echo __('Save Search'); ?></span></a></div> <div id="save-changes" class="<?php echo $save ? '' : 'hidden'; ?>" style="padding:5px; border-top: 1px dotted #777;"> - <div><input name="name" type="text" size="40" - value="<?php echo $search->isSaved() ? Format::htmlchars($search->getName()) : ''; ?>" + <div><input name="queue-name" type="text" size="40" + value="<?php echo Format::htmlchars($search->isSaved() ? $search->getName() : + $_POST['queue-name']); ?>" placeholder="<?php echo __('Search Title'); ?>"> + <?php + if ($search instanceof AdhocSearch && !$search->isSaved()) { ?> <span class="buttons"> - <button class="button" type="button" name="save" + <button class="save button" type="button" name="save-search" value="save"><i class="icon-save"></i> <?php echo $search->id ? __('Save Changes') : __('Save'); ?></button> </span> + <?php + } ?> </div> - <div class="error" id="name-error"><?php echo Format::htmlchars($errors['name']); ?></div> + <div class="error" id="name-error"><?php echo + Format::htmlchars($errors['queue-name']); ?></div> </div> + </div> +<?php +} ?> <hr/> <div> <p class="full-width"> <span class="buttons pull-left"> - <input type="reset" id="reset" value="<?php echo __('Reset'); ?>"> - <input type="button" name="cancel" class="close" - value="<?php echo __('Cancel'); ?>"> + <input type="button" name="cancel" class="close" value="<?php echo __('Cancel'); ?>"> + <?php + if ($search->isSaved()) { ?> + <input type="button" name="done" class="done" value="<?php echo + __('Done'); ?>" > + <?php + } ?> </span> <span class="buttons pull-right"> + <?php + if (!$search instanceof AdhocSearch) { ?> + <button class="save button" type="submit" name="save" value="save" + id="do_save"><i class="icon-save"></i> + <?php echo __('Save'); ?></button> + <?php + } else { ?> <button class="button" type="submit" name="submit" value="search" id="do_search"><i class="icon-search"></i> <?php echo __('Search'); ?></button> + <?php + } ?> </span> </p> </div> @@ -185,32 +235,45 @@ height:auto;"> return false; }); - $('form.savedsearch').on('keyup change paste', 'input, select, textarea', function() { - var form = $(this).closest('form'); - $this = $('#save-changes', form); - if ($this.is(":hidden")) - $this.fadeIn(); - $('a#save').find('i').removeClass('icon-caret-right').addClass('icon-caret-down'); - $('button[name=save]', form).addClass('save pending'); - $('div.error', form).html(''); + $('form#advsearch').on('keyup change paste', 'input, select, textarea', function() { + + var form = $(this).closest('form'); + $this = $('#save-changes', form); + $('button.save', form).addClass('save pending'); + $('div.error, div.error-banner', form).html('').hide(); }); $(document).on('click', 'form#advsearch input#reset', function(e) { var f = $(this).closest('form'); - $('button[name=save]', f).removeClass('save pending'); + $('button.save', f).removeClass('save pending'); $('div#save-changes', f).hide(); }); - $('button[name=save]').click(function() { + $('button[name=save-search]').click(function() { var $form = $(this).closest('form'); var id = parseInt($('input[name=id]', $form).val(), 10) || 0; - var action = '#tickets/search'; - if (id > 0) - action = action + '/'+id; + var name = $('input[name=queue-name]', $form).val(); + if (name.length) { + var action = '#tickets/search'; + if (id > 0) + action = action + '/'+id; + $form.prop('action', action+'/save'); + $form.submit(); + } else { + $('div#name-error', $form).html('<?php echo __('Name required'); + ?>').show(); + } - $form.prop('action', action+'/save'); - $form.submit(); + return false; }); + $('input.done').click(function() { + var $form = $(this).closest('form'); + var id = parseInt($('input[name=id]', $form).val(), 10) || 0; + if ($('button.save', $form).hasClass('pending')) + alert('Unsaved Changes - save or cancel to discard!'); + else + window.location.href = 'tickets.php?queue='+id; + }); }(); </script> diff --git a/include/staff/templates/queue-columns.tmpl.php b/include/staff/templates/queue-columns.tmpl.php index 233a7c411defb2f853a22d3563502719efa4f262..189ee1b3b67890a99ac55250e1c4c2e1cba0ecd2 100644 --- a/include/staff/templates/queue-columns.tmpl.php +++ b/include/staff/templates/queue-columns.tmpl.php @@ -1,3 +1,4 @@ +<div style="overflow-y: auto; height:auto; max-height: 350px;"> <table class="table"> <?php if ($queue->parent) { ?> @@ -12,24 +13,22 @@ if ($queue->parent) { ?> </td> </tr> </tbody> -<?php } - // Adhoc Advanced search does not have customizable columns, but saved - // ones do - elseif ($queue->__new__) { ?> +<?php } elseif ($queue instanceof SavedQueue) { ?> <tbody> <tr> <td colspan="3"> <input type="checkbox" name="inherit-columns" <?php - if (count($queue->columns) == 0) echo 'checked="checked"'; - if ($queue instanceof SavedSearch) echo 'disabled="disabled"'; ?> - onchange="javascript:$(this).closest('table').find('.if-not-inherited').toggle(!$(this).prop('checked'));" /> + if ($queue->useStandardColumns()) echo 'checked="checked"'; + if ($queue instanceof SavedSearch && $queue->__new__) echo 'disabled="disabled"'; ?> + onchange="javascript:$(this).closest('table').find('.if-not-inherited').toggle(!$(this).prop('checked')); + $(this).closest('table').find('.standard-columns').toggle($(this).prop('checked'));" /> <?php echo __('Use standard columns'); ?> <br /><br /> </td> </tr> </tbody> <?php } -$hidden_cols = $queue->inheritColumns() || count($queue->columns) === 0; +$hidden_cols = $queue->inheritColumns() || $queue->useStandardColumns(); ?> <tbody class="if-not-inherited <?php if ($hidden_cols) echo 'hidden'; ?>"> <tr class="header"> @@ -92,8 +91,19 @@ $hidden_cols = $queue->inheritColumns() || count($queue->columns) === 0; </td> </tr> </tbody> + <tbody class="standard-columns <?php if (!$hidden_cols) echo 'hidden'; ?>"> + <?php + foreach ($queue->getStandardColumns() as $c) { ?> + <tr> + <td nowrap><?php echo Format::htmlchars($c->heading); ?></td> + <td nowrap><?php echo Format::htmlchars($c->name); ?></td> + <td> </td> + </tr> + <?php + } ?> + </tbody> </table> - +</div> <script> +function() { var Q = setInterval(function() { @@ -102,7 +112,10 @@ var Q = setInterval(function() { clearInterval(Q); var addColumn = function(colid, info) { - if (!colid) return; + + if (!colid || $('tr#column-'+colid).length) + return; + var copy = $('#column-template').clone(), name_prefix = 'columns[' + colid + ']'; info['column_id'] = colid; @@ -119,7 +132,7 @@ var Q = setInterval(function() { $this.attr('name', name_prefix + '[' + name + ']'); }); copy.find('span').text(info['name']); - copy.attr('id', '').show().insertBefore($('#column-template')); + copy.attr('id', 'column-'+colid).show().insertBefore($('#column-template')); copy.removeClass('hidden'); if (info['trans'] !== undefined) { var input = copy.find('input[data-translate-tag]') diff --git a/include/staff/templates/queue-tickets.tmpl.php b/include/staff/templates/queue-tickets.tmpl.php index fb883ba85178ef417b974949592a7dac737a5783..37d7cced74789b85fd6aa1f6cf6b0dc720eacc30 100644 --- a/include/staff/templates/queue-tickets.tmpl.php +++ b/include/staff/templates/queue-tickets.tmpl.php @@ -122,62 +122,37 @@ return false;"> <div class="pull-left flush-left"> <h2><a href="<?php echo $refresh_url; ?>" title="<?php echo __('Refresh'); ?>"><i class="icon-refresh"></i> <?php echo - $queue->getName(); ?></a></h2> + $queue->getName(); ?></a> + <?php + if (($crit=$queue->getSupplementalCriteria())) + echo sprintf('<i class="icon-filter" + data-placement="bottom" data-toggle="tooltip" + title="%s"></i> ', + Format::htmlchars($queue->describeCriteria($crit))); + ?> + </h2> </div> <div class="configureQ"> <i class="icon-cog"></i> <div class="noclick-dropdown anchor-left"> <ul> -<?php -if ($queue->isPrivate()) { ?> <li> <a class="no-pjax" href="#" data-dialog="ajax.php/tickets/search/<?php echo urlencode($queue->getId()); ?>"><i - class="icon-fixed-width icon-save"></i> - <?php echo __('Edit'); ?></a> - </li> -<?php } -else { - if ($thisstaff->isAdmin()) { ?> - <li> - <a class="no-pjax" - href="queues.php?id=<?php echo $queue->id; ?>"><i class="icon-fixed-width icon-pencil"></i> <?php echo __('Edit'); ?></a> </li> -<?php } -# Anyone has permission to create personal sub-queues -?> <li> <a class="no-pjax" href="#" - data-dialog="ajax.php/tickets/search?parent_id=<?php - echo $queue->id; ?>"><i + data-dialog="ajax.php/tickets/search/create?pid=<?php + echo $queue->getId(); ?>"><i class="icon-fixed-width icon-plus-sign"></i> - <?php echo __('Add Personal Queue'); ?></a> - </li> -<?php -} -if ($thisstaff->isAdmin()) { ?> - <li> - <a class="no-pjax" - href="queues.php?a=sub&id=<?php echo $queue->id; ?>"><i - class="icon-fixed-width icon-level-down"></i> <?php echo __('Add Sub Queue'); ?></a> </li> - <li> - <a class="no-pjax" - href="queues.php?a=clone&id=<?php echo $queue->id; ?>"><i - class="icon-fixed-width icon-copy"></i> - <?php echo __('Clone'); ?></a> - </li> -<?php } -if ( - $queue->id > 0 - && ( - ($thisstaff->isAdmin() && $queue->parent_id) - || $queue->isPrivate() -)) { ?> +<?php + +if ($queue->id > 0 && $queue->isOwner($thisstaff)) { ?> <li class="danger"> <a class="no-pjax confirm-action" href="#" data-dialog="ajax.php/queue/<?php diff --git a/include/staff/templates/savedqueue-settings.tmpl.php b/include/staff/templates/savedqueue-settings.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..42d36d57bf1006021c67e219c57e357a83cf5568 --- /dev/null +++ b/include/staff/templates/savedqueue-settings.tmpl.php @@ -0,0 +1,63 @@ +<div style="overflow-y: auto; height:auto; max-height: 350px;"> + <div> + <div class="faded"><strong><?php echo __('Name'); ?></strong></div> + <div> + <?php + if ($queue->checkOwnership($thisstaff)) { ?> + <input name="queue-name" type="text" size="40" + value="<?php echo Format::htmlchars($queue->getName()); ?>" + placeholder="<?php echo __('Search Title'); ?>"> + <?php + } else { + echo Format::htmlchars($queue->getName()); + } ?> + </div> + <div class="error" id="name-error"><?php echo + Format::htmlchars($errors['queue-name']); ?></div> + </div> + <div> + <div class="faded"><strong><?php echo __("Quick Filter"); ?></strong></div> + <div> + <select name="filter"> + <option value="" <?php if ($queue->filter == "") + echo 'selected="selected"'; ?>>— <?php echo __('None'); ?> —</option> + <option value="::" <?php if ($queue->filter == "::") + echo 'selected="selected"'; ?>>— <?php echo __('Inherit from parent'); + if ($queue->parent + && ($qf = $queue->parent->getQuickFilterField())) + echo sprintf(' (%s)', $qf->getLabel()); ?> —</option> +<?php foreach ($queue->getSupportedFilters() as $path => $f) { + list($label, $field) = $f; +?> + <option value="<?php echo $path; ?>" + <?php if ($path == $queue->filter) echo 'selected="selected"'; ?> + ><?php echo Format::htmlchars($label); ?></option> +<?php } ?> + </select> + </div> + <div class="error"><?php + echo Format::htmlchars($errors['filter']); ?></div> + </div> + <div> + <div class="faded"><strong><?php echo __("Defaut Sorting"); ?></strong></div> + <div> + <select name="sort_id"> + <option value="" <?php if ($queue->sort_id == 0) + echo 'selected="selected"'; ?>>— <?php echo __('None'); ?> —</option> + <option value="::" <?php if ($queue->isDefaultSortInherited() && + $queue->parent) + echo 'selected="selected"'; ?>>— <?php echo __('Inherit from parent'); + if ($queue->parent + && ($sort = $queue->parent->getDefaultSort())) + echo sprintf(' (%s)', $sort->getName()); ?> —</option> +<?php foreach ($queue->getSortOptions() as $sort) { ?> + <option value="<?php echo $sort->id; ?>" + <?php if ($sort->id == $queue->sort_id) echo 'selected="selected"'; ?> + ><?php echo Format::htmlchars($sort->getName()); ?></option> +<?php } ?> + </select> + </div> + <div class="error"><?php + echo Format::htmlchars($errors['sort_id']); ?></div> + </div> +</div> diff --git a/include/upgrader/streams/core.sig b/include/upgrader/streams/core.sig index 4ad11e1f415bd953c9e8a80441aa12cdb7bbf5dc..5c4c675cad5ffbfde918a49029645fc955d772dd 100644 --- a/include/upgrader/streams/core.sig +++ b/include/upgrader/streams/core.sig @@ -1 +1 @@ -e7dfe82131b906a14f6a13163943855f +70921d5c3920ab240b08bdd55bc894c8 diff --git a/include/upgrader/streams/core/e7dfe821-70921d5c.patch.sql b/include/upgrader/streams/core/e7dfe821-70921d5c.patch.sql new file mode 100644 index 0000000000000000000000000000000000000000..3547e6698f15cb0adcff927510968cb168bb1f08 --- /dev/null +++ b/include/upgrader/streams/core/e7dfe821-70921d5c.patch.sql @@ -0,0 +1,43 @@ +/** +* @signature 70921d5c3920ab240b08bdd55bc894c8 +* @version v1.11.0 +* @title Make Public CustomQueues Configurable +* +* This patch adds staff_id to queue_columns table and queue_config table to +* allow for ability to customize public queue columns as well as additional +* settings +* +*/ + +-- Add staff_id to queue_columns table +ALTER TABLE `%TABLE_PREFIX%queue_columns` + ADD `staff_id` int(11) unsigned NOT NULL AFTER `column_id`; + +-- Set staff_id to 0 for default columns +UPDATE `%TABLE_PREFIX%queue_columns` + SET `staff_id` = 0; + +-- Add staff_id to PRIMARY KEY +ALTER TABLE `%TABLE_PREFIX%queue_columns` + DROP PRIMARY KEY, + ADD PRIMARY KEY (`queue_id`, `column_id`, `staff_id`); + +-- Set staff_id to 0 for public queues +UPDATE `%TABLE_PREFIX%queue` + SET `staff_id` = 0 + WHERE (`flags` & 1) >0; + +-- Add bridge table for public Queues staff configuration & settings +DROP TABLE IF EXISTS `%TABLE_PREFIX%queue_config`; +CREATE TABLE `%TABLE_PREFIX%queue_config` ( + `queue_id` int(11) unsigned NOT NULL, + `staff_id` int(11) unsigned NOT NULL, + `setting` text, + `updated` datetime NOT NULL, + PRIMARY KEY (`queue_id`,`staff_id`) +) DEFAULT CHARSET=utf8; + + -- Finished with patch +UPDATE `%TABLE_PREFIX%config` + SET `value` = '70921d5c3920ab240b08bdd55bc894c8' + WHERE `key` = 'schema_signature' AND `namespace` = 'core'; diff --git a/scp/ajax.php b/scp/ajax.php index 039af6b3e48d838eeabd2506dffbfc67c3055ed7..c746da68d0650c46ce0b524f789803e70d6b1582 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -177,6 +177,7 @@ $dispatcher = patterns('', url_post('^$', 'doSearch'), url_get('^/(?P<id>\d+)$', 'editSearch'), url_get('^/adhoc,(?P<key>[\w=/+]+)$', 'getAdvancedSearchDialog'), + url_get('^/create$', 'createSearch'), url_post('^/(?P<id>\d+)/save$', 'saveSearch'), url_post('^/save$', 'saveSearch'), url_delete('^/(?P<id>\d+)$', 'deleteSearch'), diff --git a/scp/js/scp.js b/scp/js/scp.js index fb2a6428a9c8e1884186968236b252f9a197eda7..b40837fc01b7cfc53eddfb940d1ae2ef38cf5b02 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -711,7 +711,9 @@ $.dialog = function (url, codes, cb, options) { } catch (e) { } $('div.body', $popup).html(resp); - $popup.effect('shake'); + if ($('#msg_error, .error-banner', $popup).length) { + $popup.effect('shake'); + } $('#msg_notice, #msg_error', $popup).delay(5000).slideUp(); $('div.tab_content[id] div.error:not(:empty)', $popup).each(function() { var div = $(this).closest('.tab_content'); diff --git a/scp/queues.php b/scp/queues.php index c2a641614e47116bcd2cd9efb0ebc38b331d20eb..ba5f33b78baf9bdaec51b1da074993030703030c 100644 --- a/scp/queues.php +++ b/scp/queues.php @@ -20,7 +20,7 @@ require('admin.inc.php'); $nav->setTabActive('settings', 'settings.php?t='.urlencode($_GET['t'])); $errors = array(); -if ($_REQUEST['id']) { +if ($_REQUEST['id'] && is_numeric($_REQUEST['id'])) { $queue = CustomQueue::lookup($_REQUEST['id']); } @@ -43,11 +43,14 @@ if ($_POST) { case 'create': $queue = CustomQueue::create(array( 'flags' => CustomQueue::FLAG_PUBLIC, - 'root' => $_POST['root'] ?: 'Ticket' + 'staff_id' => 0, + 'title' => $_POST['queue-name'], + 'root' => $_POST['root'] ?: 'T' )); if ($queue->update($_POST, $errors) && $queue->save(true)) { - $msg = sprintf(__('Successfully added %s'), Format::htmlchars($_POST['name'])); + $msg = sprintf(__('Successfully added %s'), + Format::htmlchars($queue->getName())); } elseif (!$errors['err']) { $errors['err']=sprintf(__('Unable to add %s. Correct error(s) below and try again.'), diff --git a/scp/tickets.php b/scp/tickets.php index 773744a69f47513a5febf1451c4820ce9f3826dd..2d04b89efc48ccedab6099f58bee837d72ac2b9d 100644 --- a/scp/tickets.php +++ b/scp/tickets.php @@ -110,10 +110,10 @@ if (!$ticket) { $_SESSION[$queue_key] = $queue_id; if ((int) $queue_id && !$queue) { - $queue = CustomQueue::lookup($queue_id); + $queue = SavedQueue::lookup($queue_id); } if (!$queue) { - $queue = CustomQueue::lookup($cfg->getDefaultTicketQueueId()); + $queue = SavedQueue::lookup($cfg->getDefaultTicketQueueId()); } // Set the queue_id for navigation to turn a top-level item bold @@ -456,7 +456,7 @@ $nav->setTabActive('tickets'); $nav->addSubNavInfo('jb-overflowmenu', 'customQ_nav'); // Fetch ticket queues organized by root and sub-queues -$queues = CustomQueue::queues() +$queues = SavedQueue::queues() ->filter(Q::any(array( 'flags__hasbit' => CustomQueue::FLAG_PUBLIC, 'staff_id' => $thisstaff->getId(), diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql index 45482f83f8a448904a1e054406cd9a25c83c9c61..5c1ddc7ea1a46c736dd7d05f2037925a1f75131f 100644 --- a/setup/inc/streams/core/install-mysql.sql +++ b/setup/inc/streams/core/install-mysql.sql @@ -876,11 +876,12 @@ DROP TABLE IF EXISTS `%TABLE_PREFIX%queue_columns`; CREATE TABLE `%TABLE_PREFIX%queue_columns` ( `queue_id` int(11) unsigned NOT NULL, `column_id` int(11) unsigned NOT NULL, + `staff_id` int(11) unsigned NOT NULL, `bits` int(10) unsigned NOT NULL DEFAULT '0', `sort` int(10) unsigned NOT NULL DEFAULT '1', `heading` varchar(64) DEFAULT NULL, `width` int(10) unsigned NOT NULL DEFAULT '100', - PRIMARY KEY (`queue_id`, `column_id`) + PRIMARY KEY (`queue_id`, `column_id`, `staff_id`) ) DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `%TABLE_PREFIX%queue_sort`; @@ -913,6 +914,15 @@ CREATE TABLE `%TABLE_PREFIX%queue_export` ( KEY `queue_id` (`queue_id`) ) DEFAULT CHARSET=utf8; +DROP TABLE IF EXISTS `%TABLE_PREFIX%queue_config`; +CREATE TABLE `%TABLE_PREFIX%queue_config` ( + `queue_id` int(11) unsigned NOT NULL, + `staff_id` int(11) unsigned NOT NULL, + `setting` text, + `updated` datetime NOT NULL, + PRIMARY KEY (`queue_id`,`staff_id`) +) DEFAULT CHARSET=utf8; + DROP TABLE IF EXISTS `%TABLE_PREFIX%translation`; CREATE TABLE `%TABLE_PREFIX%translation` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT,