diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index 708bb6dbdc88b3f6adadba7205cc60efb5da8b4c..2e90137a521d19134498e51e795bd2ce8eeb20e0 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -275,6 +275,20 @@ class TicketsAjaxAPI extends AjaxController { return $this->json_encode($result); } + function getAdvancedSearchDialog() { + global $cfg, $thisstaff; + + if (!$thisstaff) + Http::response(403, 'Agent login required'); + + include STAFFINC_DIR . 'templates/advanced-search.tmpl.php'; + } + + function doSearch() { + $tickets = self::_search($_POST); + $result = array(); + } + function acquireLock($tid) { global $cfg,$thisstaff; diff --git a/include/class.forms.php b/include/class.forms.php index fb5b3cf6f32136e8005a7a04256f30e9642bc10e..b0dd1913aa10ea95cac38a31cde0bed3b2d9d63a 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -420,10 +420,76 @@ class FormField { return $this->toString($this->getClean()); } + /** + * Fetches a value that represents this content in a consistent, + * searchable format. This is used by the search engine system and + * backend. + */ function searchable($value) { return Format::searchable($this->toString($value)); } + /** + * Fetches a list of options for searching. The values returned from + * this method are passed to the widget's `::render()` method so that + * the widget can be affected by this setting. For instance, date fields + * might have a 'between' search option which should trigger rendering + * of two date widgets for search results. + */ + function getSearchMethods() { + return array( + 'set' => __('has a value'), + 'set.not' => __('does not have a value'), + 'equal' => __('is'), + 'equal.not' => __('is not'), + 'contains' => __('contains'), + 'match' => __('matches'), + ); + } + + function getSearchMethodWidgets() { + return array( + 'set' => null, + 'set.not' => null, + 'equal' => array('TextboxField', array()), + 'equal.not' => array('TextboxField', array()), + 'contains' => array('TextboxField', array()), + 'match' => array('TextboxField', array( + 'validators' => function($self, $v) { + if (false === @preg_match($v, ' ')) + $self->addError(__('Cannot compile this regular expression')); + })), + ); + } + + function getSearchWidget($method) { + $methods = $this->getSearchMethodWidgets(); + $info = $methods[$method]; + if (is_array($info)) { + $class = $info[0]; + return new $class($info[1]); + } + return $info; + } + + /** + * This is used by the searching system to build a query for the search + * engine. The function should return a criteria listing to match + * content saved by the field by the `::to_database()` function. + */ + function getSearchCriteria($method, $name, $value) { + switch ($method) { + case 'search': + return array($name => $value); + case 'search.set': + return array("{$name}__isnull" => false); + case 'search.notset': + return array("{$name}__isnull" => true); + default: + throw new Exception('Search method not supported by this field'); + } + } + function getLabel() { return $this->get('label'); } /** @@ -912,6 +978,20 @@ class BooleanField extends FormField { function toString($value) { return ($value) ? __('Yes') : __('No'); } + + function getSearchMethods() { + return array( + 'set' => __('checked'), + 'set.not' => __('unchecked'), + ); + } + + function getSearchMethodWidgets() { + return array( + 'set' => null, + 'set.not' => null, + ); + } } class ChoiceField extends FormField { @@ -1029,7 +1109,26 @@ class ChoiceField extends FormField { } } return $this->_choices; - } + } + + function getSearchMethods() { + return array( + 'set' => __('has a value'), + 'set.not' => __('does not have a value'), + 'includes' => __('includes'), + ); + } + + function getSearchMethodWidgets() { + return array( + 'set' => null, + 'set.not' => null, + 'includes' => array('ChoiceField', array( + 'choices' => $this->getChoices(), + 'configuration' => array('multiselect' => true), + )), + ); + } } class DatetimeField extends FormField { diff --git a/include/class.search.php b/include/class.search.php index fd8c7e2cff06868b63439c980252d4b4719a71a0..108a77d9365264b6b9fbddee488c824e6da34807 100644 --- a/include/class.search.php +++ b/include/class.search.php @@ -629,3 +629,113 @@ Signal::connect('system.install', array('MysqlSearchBackend', '__init')); MysqlSearchBackend::register(); + +// Advanced search special fields + +class HelpTopicChoiceField extends ChoiceField { + function hasIdValue() { + return true; + } + + function getChoices() { + return Topic::getHelpTopics(false, Topic::DISPLAY_DISABLED); + } +} + +require_once INCLUDE_DIR . 'class.dept.php'; +class DepartmentChoiceField extends ChoiceField { + function getChoices() { + return Dept::getDepartments(); + } + + function getSearchMethods() { + return array( + 'is' => __('is'), + 'isnot' => __('is not'), + ); + } + + function getSearchMethodWidgets() { + return array( + 'is' => array('ChoiceField', array( + 'choices' => $this->getChoices(), + 'configuration' => array('multiselect' => true), + )), + 'isnot' => array('ChoiceField', array( + 'choices' => $this->getChoices(), + 'configuration' => array('multiselect' => true), + )), + ); + } +} + +class AssigneeChoiceField extends ChoiceField { + function getChoices() { + $items = array( + 'me' => __('Me'), + 'myteam' => __('One of my teams'), + ); + foreach (Staff::getStaffMembers() as $id=>$name) { + $items['s' . $id] = $name; + } + foreach (Team::getTeams() as $id=>$name) { + $items['t' . $id] = $name; + } + return $items; + } + + function getSearchMethods() { + return array( + 'assigned' => __('assigned'), + 'assigned.not' => __('unassigned'), + 'is' => __('is'), + 'isnot' => __('is not'), + ); + } + + function getSearchMethodWidgets() { + return array( + 'assigned' => null, + 'assigned.not' => null, + 'is' => array('ChoiceField', array( + 'choices' => $this->getChoices(), + 'configuration' => array('multiselect' => true), + )), + 'isnot' => array('ChoiceField', array( + 'choices' => $this->getChoices(), + 'configuration' => array('multiselect' => true), + )), + ); + } +} + +class TicketStatusChoiceField extends SelectionField { + static $widget = 'ChoicesWidget'; + + function getList() { + return new TicketStatusList( + DynamicList::lookup( + array('type' => 'ticket-status')) + ); + } + + function getSearchMethods() { + return array( + 'is' => __('is'), + 'isnot' => __('is not'), + ); + } + + function getSearchMethodWidgets() { + return array( + 'is' => array('ChoiceField', array( + 'choices' => $this->getChoices(), + 'configuration' => array('multiselect' => true), + )), + 'isnot' => array('ChoiceField', array( + 'choices' => $this->getChoices(), + 'configuration' => array('multiselect' => true), + )), + ); + } +} diff --git a/include/staff/templates/advanced-search.tmpl.php b/include/staff/templates/advanced-search.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..de13b7a088dcc4eb26ef123e9008b17998a98296 --- /dev/null +++ b/include/staff/templates/advanced-search.tmpl.php @@ -0,0 +1,86 @@ +<?php +$matches=Filter::getSupportedMatches(); +$basicForm = array( + 'status' => new TicketStateField(array( + 'label'=>__('Status'), + )), + 'dept' => new ChoiceField(array( + 'label'=>__('Department'), + 'choices' => Dept::getDepartments(), + )), + 'assignee' => new ChoiceField(array( + 'label' => __('Assigned To'), + 'choices' => Staff::getStaffMembers(), + )), + 'closed' => new ChoiceField(array( + 'label' => __('Closed By'), + 'choices' => Staff::getStaffMembers(), + )), +); + +$basic = array( + 'status' => new TicketStatusChoiceField(array( + 'label' => __('Ticket Status'), + )), + 'dept' => new DepartmentChoiceField(array( + 'label' => __('Department'), + )), + 'assignee' => new AssigneeChoiceField(array( + 'label' => __('Assignee'), + )), + 'topic' => new HelpTopicChoiceField(array( + 'label' => __('Help Topic'), + )), + 'created' => new DateTimeField(array( + 'label' => __('Created'), + )), +); +?> +<div id="advanced-search"> +<h3><?php echo __('Advanced Ticket Search');?></h3> +<a class="close" href=""><i class="icon-remove-circle"></i></a> +<hr/> +<form action="ajax.php/tickets/advanced-search" method="post" name="search"> + <input type="hidden" name="a" value="search"> + <fieldset class="query"> + <input type="input" id="query" name="query" size="20" placeholder="<?php echo __('Keywords') . ' — ' . __('Optional'); ?>"> + </fieldset> + +<?php +foreach ($basic as $name=>$field) { ?> + <fieldset> + <input type="checkbox" name="fields[]" value="<?php echo $name; ?>" + onchange="javascript: + $('#search-<?php echo $name; ?>').slideToggle($(this).is(':checked')); + $('#method-<?php echo $name; ?>-' + $('#search-<?php echo $name; ?>').find(':selected').val()).slideDown('fast');"> + <?php echo $field->getLabel(); ?> + <div class="search-dropdown" style="display:none" id="search-<?php echo $name; ?>"> + <select style="min-width:150px" name="method-<?php echo $name; ?>" onchange="javascript: + $(this).parent('div').find('.search-value').slideUp('fast'); + $('#method-<?php echo $name; ?>-' + $(this).find(':selected').val()).slideDown('fast'); +"> +<?php foreach ($field->getSearchMethods() as $method=>$label) { ?> + <option value="<?php echo $method; ?>"><?php echo $label; ?></option> +<?php } ?> + </select> +<?php foreach ($field->getSearchMethods() as $method=>$label) { ?> + <span class="search-value" style="display:none;" id="method-<?php echo $name . '-' . $method; ?>"> +<?php + if ($f = $field->getSearchWidget($method)) + print $f->render(); +?> + </span> +<?php } ?> + </div> + </fieldset> +<?php +} ?> +</form> + +<hr/> +<div> + <div class="pull-right"> + <button><i class="icon-save"></i> Save Search</button> + </div> +</div> +<link rel="stylesheet" type="text/css" href="<?php echo ROOT_PATH; ?>css/jquery.multiselect.css"/> diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php index 1c5b5f003c453e4c131517c908d8efb91836a15f..f27de7cf3f5a79c9b902f0ecc72a97615651bba7 100644 --- a/include/staff/tickets.inc.php +++ b/include/staff/tickets.inc.php @@ -316,7 +316,9 @@ if ($results) { <td><input type="text" id="basic-ticket-search" name="query" size=30 value="<?php echo Format::htmlchars($_REQUEST['query']); ?>" autocomplete="off" autocorrect="off" autocapitalize="off"></td> <td><input type="submit" name="basic_search" class="button" value="<?php echo __('Search'); ?>"></td> - <td> <a href="#" id="go-advanced">[<?php echo __('advanced'); ?>]</a> <i class="help-tip icon-question-sign" href="#advanced"></i></td> + <td> <a href="#" onclick="javascript: + $.dialog('ajax.php/tickets/advanced-search', 201);" + >[<?php echo __('advanced'); ?>]</a> <i class="help-tip icon-question-sign" href="#advanced"></i></td> </tr> </table> </form> @@ -536,142 +538,6 @@ if ($results) { </p> <div class="clear"></div> </div> - -<div class="dialog" style="display:none;" id="advanced-search"> - <h3><?php echo __('Advanced Ticket Search');?></h3> - <a class="close" href=""><i class="icon-remove-circle"></i></a> - <hr/> - <form action="tickets.php" method="post" id="search" name="search"> - <input type="hidden" name="a" value="search"> - <fieldset class="query"> - <input type="input" id="query" name="query" size="20" placeholder="<?php echo __('Keywords') . ' — ' . __('Optional'); ?>"> - </fieldset> - <fieldset class="span6"> - <label for="statusId"><?php echo __('Statuses');?>:</label> - <select id="statusId" name="statusId"> - <option value="">— <?php echo __('Any Status');?> —</option> - <?php - foreach (TicketStatusList::getStatuses( - array('states' => array('open', 'closed'))) as $s) { - echo sprintf('<option data-state="%s" value="%d">%s</option>', - $s->getState(), $s->getId(), __($s->getName())); - } - ?> - </select> - </fieldset> - <fieldset class="span6"> - <label for="deptId"><?php echo __('Departments');?>:</label> - <select id="deptId" name="deptId"> - <option value="">— <?php echo __('All Departments');?> —</option> - <?php - if(($mydepts = $thisstaff->getDepts()) && ($depts=Dept::getDepartments())) { - foreach($depts as $id =>$name) { - if(!in_array($id, $mydepts)) continue; - echo sprintf('<option value="%d">%s</option>', $id, $name); - } - } - ?> - </select> - </fieldset> - <fieldset class="span6"> - <label for="flag"><?php echo __('Flags');?>:</label> - <select id="flag" name="flag"> - <option value="">— <?php echo __('Any Flags');?> —</option> - <?php - if (!$cfg->showAnsweredTickets()) { ?> - <option data-state="open" value="answered"><?php echo __('Answered');?></option> - <?php - } ?> - <option data-state="open" value="overdue"><?php echo __('Overdue');?></option> - </select> - </fieldset> - <fieldset class="owner span6"> - <label for="assignee"><?php echo __('Assigned To');?>:</label> - <select id="assignee" name="assignee"> - <option value="">— <?php echo __('Anyone');?> —</option> - <option value="0">— <?php echo __('Unassigned');?> —</option> - <option value="s<?php echo $thisstaff->getId(); ?>"><?php echo __('Me');?></option> - <?php - if(($users=Staff::getStaffMembers())) { - echo '<OPTGROUP label="'.sprintf(__('Agents (%d)'),count($users)).'">'; - foreach($users as $id => $name) { - $k="s$id"; - echo sprintf('<option value="%s">%s</option>', $k, $name); - } - echo '</OPTGROUP>'; - } - - if(($teams=Team::getTeams())) { - echo '<OPTGROUP label="'.__('Teams').' ('.count($teams).')">'; - foreach($teams as $id => $name) { - $k="t$id"; - echo sprintf('<option value="%s">%s</option>', $k, $name); - } - echo '</OPTGROUP>'; - } - ?> - </select> - </fieldset> - <fieldset class="span6"> - <label for="topicId"><?php echo __('Help Topics');?>:</label> - <select id="topicId" name="topicId"> - <option value="" selected >— <?php echo __('All Help Topics');?> —</option> - <?php - if($topics=Topic::getHelpTopics()) { - foreach($topics as $id =>$name) - echo sprintf('<option value="%d" >%s</option>', $id, $name); - } - ?> - </select> - </fieldset> - <fieldset class="owner span6"> - <label for="staffId"><?php echo __('Closed By');?>:</label> - <select id="staffId" name="staffId"> - <option value="0">— <?php echo __('Anyone');?> —</option> - <option value="<?php echo $thisstaff->getId(); ?>"><?php echo __('Me');?></option> - <?php - if(($users=Staff::getStaffMembers())) { - foreach($users as $id => $name) - echo sprintf('<option value="%d">%s</option>', $id, $name); - } - ?> - </select> - </fieldset> - <fieldset class="date_range"> - <label><?php echo __('Date Range').' — '.__('Create Date');?>:</label> - <input class="dp" type="input" size="20" name="startDate"> - <span class="between"><?php echo __('TO');?></span> - <input class="dp" type="input" size="20" name="endDate"> - </fieldset> - <?php - $tform = TicketForm::objects()->one(); - echo $tform->getForm()->getMedia(); - foreach ($tform->getInstance()->getFields() as $f) { - if (!$f->hasData()) - continue; - elseif (!$f->getImpl()->hasSpecialSearch()) - continue; - ?><fieldset class="span6"> - <label><?php echo $f->getLabel(); ?>:</label><div><?php - $f->render('search'); ?></div> - </fieldset> - <?php } ?> - <hr/> - <div id="result-count" class="clear"></div> - <p> - <span class="buttons pull-right"> - <input type="submit" value="<?php echo __('Search');?>"> - </span> - <span class="buttons pull-left"> - <input type="reset" value="<?php echo __('Reset');?>"> - <input type="button" value="<?php echo __('Cancel');?>" class="close"> - </span> - <span class="spinner"> - <img src="./images/ajax-loader.gif" width="16" height="16"> - </span> - </p> - </form> -</div> <script type="text/javascript"> $(function() { $(document).off('.tickets'); diff --git a/scp/ajax.php b/scp/ajax.php index 4ca0abf850e615d3f09479db47a904ecd511169c..ac53db3f710154af18ae70a6751529dbcbeecf92 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -137,6 +137,8 @@ $dispatcher = patterns('', url_get('^(?P<tid>\d+)/add-collaborator/(?P<uid>\d+)$', 'addCollaborator'), url_get('^(?P<tid>\d+)/add-collaborator/auth:(?P<bk>\w+):(?P<id>.+)$', 'addRemoteCollaborator'), url('^(?P<tid>\d+)/add-collaborator$', 'addCollaborator'), + url_get('^advanced-search', 'getAdvancedSearchDialog'), + url_post('^advanced-search', 'doSearch'), url_get('^(?P<tid>\d+)/forms/manage$', 'manageForms'), url_post('^(?P<tid>\d+)/forms/manage$', 'updateForms'), url_get('^(?P<tid>\d+)/canned-resp/(?P<cid>\w+).(?P<format>json|txt)', 'cannedResponse'), diff --git a/scp/js/scp.js b/scp/js/scp.js index 74061b615bf10aca5cc6a603bd196e7a20d77bd1..6649ddeb2613a0d918895bfea4d0bff2cf378e37 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -338,26 +338,12 @@ var scp_prep = function() { return false; }); - /* advanced search */ - $('.dialog#advanced-search').css({ - top : ($(window).height() / 6), - left : ($(window).width() / 2 - 300) - }); - /* loading ... */ $("#loading").css({ top : ($(window).height() / 3), left : ($(window).width() - $("#loading").outerWidth()) / 2 }); - $('#go-advanced').click(function(e) { - e.preventDefault(); - $('#result-count').html(''); - $.toggleOverlay(true); - $('#advanced-search').show(); - }); - - $('#advanced-search').delegate('#statusId, #flag', 'change', function() { switch($(this).children('option:selected').data('state')) { case 'closed':