diff --git a/include/ajax.search.php b/include/ajax.search.php new file mode 100644 index 0000000000000000000000000000000000000000..b0d99f18fac9f8b323908e62c26d9a9bb6774b3c --- /dev/null +++ b/include/ajax.search.php @@ -0,0 +1,153 @@ +<?php +/********************************************************************* + ajax.search.php + + AJAX interface for searches, queue management, etc. + + Jared Hancock <jared@osticket.com> + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2014 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +if(!defined('INCLUDE_DIR')) die('403'); + +include_once(INCLUDE_DIR.'class.ticket.php'); +require_once(INCLUDE_DIR.'class.ajax.php'); + +class SearchAjaxAPI extends AjaxController { + + function getAdvancedSearchDialog() { + global $thisstaff; + + if (!$thisstaff) + Http::response(403, 'Agent login required'); + + $search = SavedSearch::create(); + $form = $search->getForm(); + if (isset($_SESSION['advsearch'])) + $form->loadState($_SESSION['advsearch']); + $matches = Filter::getSupportedMatches(); + + include STAFFINC_DIR . 'templates/advanced-search.tmpl.php'; + } + + function addField($name) { + global $thisstaff; + + if (!$thisstaff) + Http::response(403, 'Agent login required'); + + } + + function doSearch() { + global $thisstaff; + + $search = SavedSearch::create(); + + // Add "other" fields (via $_POST['other'][]) + + $form = $search->getForm($_POST); + if (!$form->isValid()) { + $matches = Filter::getSupportedMatches(); + include STAFFINC_DIR . 'templates/advanced-search.tmpl.php'; + return; + } + $_SESSION['advsearch'] = $form->getState(); + + Http::response(200, $this->encode(array( + 'redirect' => 'tickets.php?advanced', + ))); + } + + function saveSearch($id) { + global $thisstaff; + + $search = SavedSearch::lookup($id); + if (!$search || !$search->checkAccess($thisstaff)) + Http::response(404, 'No such saved search'); + elseif (!$thisstaff) + Http::response(403, 'Agent login is required'); + + return self::_saveSearch($search); + } + + function _saveSearch($search) { + $data = array(); + foreach ($_POST['form'] as $id=>$info) { + $name = $info['name']; + if (substr($name, -2) == '[]') + $data[substr($name, 0, -2)][] = $info['value']; + else + $data[$name] = $info['value']; + } + $form = $search->getForm($data); + if (!$data || !$form->isValid()) { + Http::response(422, 'Validation errors exist on form'); + } + + $search->config = JsonDataEncoder::encode($form->getState()); + if (isset($_POST['name'])) + $search->title = $_POST['name']; + if (!$search->save()) { + Http::response(500, 'Internal error. Unable to update search'); + } + Http::response(201, $this->encode(array( + 'id' => $search->id, + 'title' => $search->title, + ))); + } + + function createSearch() { + global $thisstaff; + + if (!$thisstaff) + Http::response(403, 'Agent login is required'); + + $search = SavedSearch::create(); + $search->staff_id = $thisstaff->getId(); + return self::_saveSearch($search); + } + + function loadSearch($id) { + global $thisstaff; + + if (!$thisstaff) { + Http::response(403, 'Agent login is required'); + } + elseif (!($search = SavedSearch::lookup($id))) { + Http::response(404, 'No such saved search'); + } + + $form = $search->getForm(); + if ($state = JsonDataParser::parse($search->config)) + $form->loadState($state); + + $matches = Filter::getSupportedMatches(); + include STAFFINC_DIR . 'templates/advanced-search.tmpl.php'; + } + + function deleteSearch($id) { + global $thisstaff; + + if (!$thisstaff) { + Http::response(403, 'Agent login is required'); + } + elseif (!($search = SavedSearch::lookup($id))) { + Http::response(404, 'No such saved search'); + } + elseif (!$search->delete()) { + Http::response(500, 'Unable to delete search'); + } + + Http::response(200, $this->encode(array( + 'id' => $search->id, + 'success' => true, + ))); + } +} diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index 2e90137a521d19134498e51e795bd2ce8eeb20e0..708bb6dbdc88b3f6adadba7205cc60efb5da8b4c 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -275,20 +275,6 @@ 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 fb0dfff84710b77122f884001cf3b1b6a8011194..aa514acd777d35846b53ea21d5c9e17659cb5a5b 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -2006,10 +2006,10 @@ class InlineFormField extends FormField { return ob_get_clean(); } - function getInlineForm() { + function getInlineForm($data=false) { $form = $this->get('form'); if (is_array($form)) { - $form = new Form($form); + $form = new Form($form, $data ?: $this->value ?: $this->getSource()); } return $form; } diff --git a/include/class.search.php b/include/class.search.php index 108a77d9365264b6b9fbddee488c824e6da34807..e7647cc3711330adfd9fa5566b895c686b231aa3 100644 --- a/include/class.search.php +++ b/include/class.search.php @@ -630,6 +630,187 @@ Signal::connect('system.install', MysqlSearchBackend::register(); +// Saved search system + +/** + * + * Fields: + * id - (int:unsigned:auto:pk) unique identifier + * flags - (int:unsigned) flags for this queue + * staff_id - (int:unsigned) Agent to whom this queue belongs (can be null + * for public saved searches) + * title - (text:60) name of the queue + * config - (text) JSON encoded search configuration for the queue + * created - (date) date initially created + * updated - (date:auto_update) time of last update + */ +class SavedSearch extends VerySimpleModel { + + static $meta = array( + 'table' => 'ost_queue', # QUEUE_TABLE + 'pk' => array('id'), + 'ordering' => array('sort'), + ); + + const FLAG_PUBLIC = 0x0001; + const FLAG_QUEUE = 0x0002; + + static function forStaff(Staff $agent) { + return static::objects()->filter(Q::any(array( + 'staff_id' => $agent->getId(), + 'flags__hasbit' => self::FLAG_PUBLIC, + ))); + } + + function getForm($source=false) { + // XXX: Ensure that the UIDs generated for these fields are + // consistent between requests + FormField::$uid = 1000; + + $searchable = $this->getCurrentSearchFields($source); + $fields = array( + 'keywords' => new TextboxField(array( + 'configuration' => array( + 'size' => 40, + 'length' => 400, + 'classes' => 'full-width headline', + 'placeholder' => __('Keywords — Optional'), + ), + )), + ); + foreach ($searchable as $name=>$field) { + $fields = array_merge($fields, $this->getSearchField($field, $name)); + } + $form = new Form($fields, $source); + $form->addValidator(function($form) { + $selected = 0; + foreach ($form->getFields() as $F) { + if (substr($F->get('name'), -7) == '+search' && $F->getClean()) + $selected += 1; + } + if (!$selected) + $form->addError('No fields selected for searching'); + }); + return $form; + } + + function getCurrentSearchFields($source=false) { + $core = array( + 'state' => new TicketStateChoiceField(array( + 'label' => __('State'), + )), + 'status_id' => new TicketStatusChoiceField(array( + 'label' => __('Status'), + )), + 'flags' => new TicketFlagChoiceField(array( + 'label' => __('Flags'), + )), + 'dept_id' => new DepartmentChoiceField(array( + 'label' => __('Department'), + )), + 'assignee' => new AssigneeChoiceField(array( + 'label' => __('Assignee'), + )), + 'topic_id' => new HelpTopicChoiceField(array( + 'label' => __('Help Topic'), + )), + 'created' => new DateTimeField(array( + 'label' => __('Created'), + )), + 'duedate' => new DateTimeField(array( + 'label' => __('Due Date'), + )), + ); + + // TODO: Add "other" fields to the basic set + + return $core; + } + + function getSearchField($field, $name) { + $pieces = array(); + $pieces["{$name}+search"] = new BooleanField(array( + 'configuration' => array('desc' => $field->get('label')) + )); + $methods = $field->getSearchMethods(); + $pieces["{$name}+method"] = new ChoiceField(array( + 'choices' => $methods, + 'default' => key($methods), + 'visibility' => new VisibilityConstraint(new Q(array( + "{$name}+search__eq" => true, + )), VisibilityConstraint::HIDDEN), + )); + foreach ($field->getSearchMethodWidgets() as $m=>$w) { + if (!$w) + continue; + list($class, $args) = $w; + $args['required'] = true; + $args['visibility'] = new VisibilityConstraint(new Q(array( + "{$name}+method__eq" => $m, + )), VisibilityConstraint::HIDDEN); + $pieces["{$name}+{$m}"] = new $class($args); + } + return $pieces; + } + + function mangleQuerySet(QuerySet $qs, $form=false) { + $form = $form ?: $this->getForm(); + $searchable = $this->getCurrentSearchFields($form->getSource()); + + // Figure out fields to search on + foreach ($form->getFields() as $f) { + if (substr($f->get('name'), -7) == '+search' && $f->getClean()) { + $name = substr($f->get('name'), 0, -7); + // Determine the search method and fetch the original field + if (($M = $form->getField("{$name}+method")) + && ($method = $M->getClean()) + && ($field = $searchable[$name]) + ) { + // Request the field to generate a search Q for the + // search method and given value + $value = null; + if ($value = $form->getField("{$name}+{$method}")) + $value = $value->getClean(); + + // Add the criteria to the QuerySet + if ($Q = $field->getSearchQ($method, $value, $name)) + $qs = $qs->filter($Q); + } + } + } + return $qs; + } + + function checkAccess(Staff $agent) { + return $agent->getId() == $this->staff_id + || $this->hasFlag(self::FLAG_PUBLIC); + } + + protected function hasFlag($flag) { + return $this->get('flag') & $flag !== 0; + } + + protected function clearFlag($flag) { + return $this->set('flag', $this->get('flag') & ~$flag); + } + + protected function setFlag($flag) { + return $this->set('flag', $this->get('flag') | $flag); + } + + static function create($vars=array()) { + $inst = parent::create($vars); + $inst->created = SqlFunction::NOW(); + return $inst; + } + + function save($refetch=false) { + if ($this->dirty) + $this->updated = SqlFunction::NOW(); + return parent::save($refetch || $this->dirty); + } +} + // Advanced search special fields class HelpTopicChoiceField extends ChoiceField { @@ -650,21 +831,8 @@ class DepartmentChoiceField extends ChoiceField { 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), - )), + 'includes' => __('is'), + '!includes' => __('is not'), ); } } @@ -672,8 +840,8 @@ class DepartmentChoiceField extends ChoiceField { class AssigneeChoiceField extends ChoiceField { function getChoices() { $items = array( - 'me' => __('Me'), - 'myteam' => __('One of my teams'), + 'M' => __('Me'), + 'T' => __('One of my teams'), ); foreach (Staff::getStaffMembers() as $id=>$name) { $items['s' . $id] = $name; @@ -686,27 +854,118 @@ class AssigneeChoiceField extends ChoiceField { function getSearchMethods() { return array( - 'assigned' => __('assigned'), - 'assigned.not' => __('unassigned'), - 'is' => __('is'), - 'isnot' => __('is not'), + 'assigned' => __('assigned'), + '!assigned' => __('unassigned'), + 'includes' => __('includes'), + '!includes' => __('does not include'), ); } function getSearchMethodWidgets() { return array( 'assigned' => null, - 'assigned.not' => null, - 'is' => array('ChoiceField', array( + '!assigned' => null, + 'includes' => array('ChoiceField', array( 'choices' => $this->getChoices(), 'configuration' => array('multiselect' => true), )), - 'isnot' => array('ChoiceField', array( + '!includes' => array('ChoiceField', array( 'choices' => $this->getChoices(), 'configuration' => array('multiselect' => true), )), ); } + + function getSearchQ($method, $value, $name=false) { + global $thisstaff; + + $Q = new Q(); + switch ($method) { + case '!assigned': + $Q->negate(); + case 'assigned': + $Q->add(array('team_id__gt' => 0, + 'staff_id__gt' => 0)); + break; + case '!includes': + $Q->negate(); + case 'includes': + $teams = $agents = array(); + foreach ($value as $id => $ST) { + switch ($id[0]) { + case 'M': + $agents[] = $thisstaff->getId(); + break; + case 's': + $agents[] = (int) substr($id, 1); + break; + case 'T': + $teams = array_merge($thisstaff->getTeams()); + break; + case 't': + $teams[] = (int) substr($id, 1); + break; + } + } + $constraints = array(); + if ($teams) + $constraints['team_id__in'] = $teams; + if ($agents) + $constraints['staff_id__in'] = $agents; + $Q->add(Q::any($constraints)); + } + return $Q; + } +} + +class TicketStateChoiceField extends ChoiceField { + function getChoices() { + return array( + 'open' => __('Open'), + 'closed' => __('Closed'), + 'archived' => __('Archived'), + 'deleted' => __('Deleted'), + ); + } + + function getSearchMethods() { + return array( + 'includes' => __('is'), + '!includes' => __('is not'), + ); + } + + function getSearchQ($method, $value, $name=false) { + return parent::getSearchQ($method, $value, 'status__state'); + } +} + +class TicketFlagChoiceField extends ChoiceField { + function getChoices() { + return array( + 'isanswered' => __('Answered'), + 'isoverdue' => __('Overdue'), + ); + } + + function getSearchMethods() { + return array( + 'includes' => __('is'), + '!includes' => __('is not'), + ); + } + + function getSearchQ($method, $value, $name=false) { + $Q = new Q(); + if (isset($value['isanswered'])) + $Q->add(array('isanswered' => 1)); + if (isset($value['isoverdue'])) + $Q->add(array('isoverdue' => 1)); + if ($method == '!includes') + $Q->negate(); + if ($Q->constraints) + return $Q; + } } class TicketStatusChoiceField extends SelectionField { @@ -721,21 +980,8 @@ class TicketStatusChoiceField extends SelectionField { 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), - )), + 'includes' => __('is'), + '!includes' => __('is not'), ); } } diff --git a/include/staff/templates/advanced-search.tmpl.php b/include/staff/templates/advanced-search.tmpl.php index de13b7a088dcc4eb26ef123e9008b17998a98296..d8d0b391e519624234ae15e99a722016b129b153 100644 --- a/include/staff/templates/advanced-search.tmpl.php +++ b/include/staff/templates/advanced-search.tmpl.php @@ -1,86 +1,164 @@ <?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"> +<form action="#tickets/search" method="post" name="search"> +<div class="row"> +<div class="span6"> <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 ($form->errors(true) ?: array() as $message) { + ?><div class="error-banner"><?php echo $message;?></div><?php +} +foreach ($form->getFields() as $name=>$field) { ?> + <fieldset id="field<?php echo $field->getWidget()->id; + ?>" <?php if (!$field->isVisible()) echo 'style="display:none;"'; ?>> + <?php echo $field->render(); ?> + <?php foreach ($field->errors() as $E) { + ?><div class="error"><?php echo $E; ?></div><?php + } ?> + </fieldset> +<?php } +?> +<hr/> +<select name="new-field" style="max-width: 100%;"> + <option value="">— <?php echo __('Add Other Field'); ?> —</option> <?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; ?>"> +foreach ($matches as $name => $fields) { ?> + <optgroup label="<?php echo $name; ?>"> <?php - if ($f = $field->getSearchWidget($method)) - print $f->render(); -?> - </span> + foreach ($fields as $id => $desc) { ?> + <option value="<?php echo $id; ?>"><?php echo $desc; ?></option> +<?php } ?> + </optgroup> <?php } ?> - </div> +</select> + +</div> +<div class="span6" style="border-left: 1px solid #888;"> +<div style="margin-bottom: 0.5em;"><b style="font-size: 110%;"><?php echo __('Saved Searches'); ?></b></div> +<div id="saved-searches" class="accordian"> +<?php foreach (SavedSearch::forStaff($thisstaff) as $S) { ?> + <dt class="saved-search"> + <a href="#" class="load-search"><?php echo $S->title; ?> + <i class="icon-chevron-down pull-right"></i> + </a> + </dt> + <dd> + <span> + <button onclick="javascript:$(this).closest('form').attr({ +'method': 'get', 'action': '#tickets/search/<?php echo $S->id; ?>'});"><i class="icon-chevron-left"></i> Load</button> + <?php if ($thisstaff->isAdmin()) { ?> + <button><i class="icon-bullhorn"></i> <?php echo __('Publish'); ?></button> + <?php } ?> + <button onclick="javascript: +$.ajax({ + url: 'ajax.php/tickets/search/<?php echo $S->id; ?>', + type: 'POST', + data: {'form': $(this).closest('.dialog').find('form[name=search]').serializeArray()}, + dataType: 'json', + success: function(json) { + if (!json.id) + return; + $('<dt>').effect('highlight'); + } +}); +return false; +"><i class="icon-save"></i> <?php echo __('Update'); ?></button> + </span> + <span class="pull-right"> + <button title="<?php echo __('Delete'); ?>" onclick="javascript: + if (!confirm(__('You sure?'))) return false; + var that = this; + $.ajax({ + 'url': 'ajax.php/tickets/search/<?php echo $S->id; ?>', + 'type': 'delete', + 'dataType': 'json', + 'success': function(json) { + if (json.success) { + $(that).closest('dd').prev('dt').slideUp().next('dd').slideUp(); + } + } + }); + return false; +"><i class="icon-trash"></i></button> + </span> + </dd> +<?php } ?> +</div> +<div> + <form method="post"> + <fieldset> + <input name="title" type="text" size="30" placeholder="Enter a title for the search"/> + <span class="action-buttons"> + <span class="action-button"> + <a href="#tickets/search/create" onclick="javascript: +$.ajax({ + url: 'ajax.php/' + $(this).attr('href').substr(1), + type: 'POST', + data: {'name': $(this).closest('form').find('[name=title]').val(), + 'form': $(this).closest('.dialog').find('form[name=search]').serializeArray()}, + dataType: 'json', + success: function(json) { + if (!json.id) + return; + $('<dt>') + .append($('<a>').text(' ' + json.title) + .prepend($('<i>').addClass('icon-chevron-left')) + ).appendTo($('#saved-searches')); + } +}); +return false; +"><i class="icon-save"></i> <?php echo __('Save'); ?></a> + </span> + <span class="action-button pull-right" data-dropdown="#save-dropdown-more"> + <i class="icon-caret-down pull-right"></i> + </span> + </span> </fieldset> -<?php -} ?> -</form> + <div id="save-dropdown-more" class="action-dropdown anchor-right"> + <ul> + <li><a href="#queue/create"> + <i class="icon-list"></i> <?php echo __('Create Queue'); ?></a> + </li> + </ul> + </div> +</div> +</div> +</div> <hr/> <div> - <div class="pull-right"> - <button><i class="icon-save"></i> Save Search</button> + <div id="search-hint" class="pull-left"> + </div> + <div class="buttons pull-right"> + <button class="button" id="do_search"><i class="icon-search"></i> <?php echo __('Search'); ?></button> </div> </div> + +</form> + <link rel="stylesheet" type="text/css" href="<?php echo ROOT_PATH; ?>css/jquery.multiselect.css"/> + +<script type="text/javascript"> +$(function() { + $('#advanced-search [data-dropdown]').dropdown(); + + var I = setInterval(function() { + var A = $('#saved-searches.accordian'); + if (!A.length) return; + clearInterval(I); + + var allPanels = $('dd', A).hide(); + $('dt > a', A).click(function() { + $('dt', A).removeClass('active'); + allPanels.slideUp(); + $(this).parent().addClass('active').next().slideDown(); + return false; + }); + }, 200); +}); +</script> diff --git a/scp/ajax.php b/scp/ajax.php index ac53db3f710154af18ae70a6751529dbcbeecf92..d040343e28995e0601cd50836e7577939b428b92 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -137,8 +137,6 @@ $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'), @@ -147,7 +145,16 @@ $dispatcher = patterns('', url_get('^status/(?P<status>\w+)(?:/(?P<sid>\d+))?$', 'changeSelectedTicketsStatus'), url_post('^status/(?P<state>\w+)$', 'setSelectedTicketsStatus'), url_get('^lookup', 'lookup'), - url_get('^search', 'search') + url('^search', patterns('ajax.search.php:SearchAjaxAPI', + url_get('^$', 'getAdvancedSearchDialog'), + url_post('^$', 'doSearch'), + url_get('^quick$', 'doQuickSearch'), + url_get('^/(?P<id>\d+)$', 'loadSearch'), + url_post('^/(?P<id>\d+)$', 'saveSearch'), + url_delete('^/(?P<id>\d+)$', 'deleteSearch'), + url_post('^/create$', 'createSearch'), + url_get('^/field/(?P<id>\d+)$', 'getField') + )) )), url('^/collaborators/', patterns('ajax.tickets.php:TicketsAjaxAPI', url_get('^(?P<cid>\d+)/view$', 'viewCollaborator'), diff --git a/scp/css/scp.css b/scp/css/scp.css index 38f3484ec7a97eeb396196469ec6ee2a7d586e09..686651801f540107b402a9ac46a1cfb4ea3b8d4d 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -39,6 +39,9 @@ div#header a { .full-width { width: 100%; } +.headline { + margin-bottom: 15px; +} .search-input { height: 20px; @@ -553,7 +556,7 @@ a.print { color:#000; } -#actions button, .button { padding:2px 5px 3px; margin-right:10px; color:#777;} +#actions button, .button { padding:2px 5px 3px; margin-right:10px; color:#333;} .btn_sm { padding:2px 5px; @@ -1504,6 +1507,7 @@ time { .dialog fieldset input { border:1px solid #ccc; background:#fff; + padding: 3px; } .dialog fieldset span.between { @@ -1524,18 +1528,15 @@ time { cursor: move; } -#advanced-search fieldset.span6 { - display: inline-block; - width: 49%; - margin-bottom: 5px; +.row { + display: table-row; } -#advanced-search fieldset label { - display: block; -} -#advanced-search fieldset.span6 select, -#advanced-search fieldset.span6 input { - max-width: 100%; - min-width: 75%; + +.row .span6 { + display: table-cell; + width: 48%; + padding: 5px 10px; + vertical-align: top; } #advanced-search .query input { @@ -1570,9 +1571,14 @@ time { text-align:center; } +.search-dropdown { + padding-left: 19px; +} + .dialog input[type="submit"], .dialog input[type="reset"], -.dialog input[type="button"] { +.dialog input[type="button"], +.dialog button.button { display:inline-block; margin:0; height:24px; @@ -2072,3 +2078,57 @@ button a:hover { td.indented { padding-left: 20px; } +.secondary_lang { + padding:3px 0; + margin: 3px 0; + border-bottom: 1px dotted #ccc; +} +.saved-search { + padding: 5px; +} + +.saved-search + .saved-search { + border-top: 1px dotted #ccc; +} + +.accordian { + margin-bottom: 10px; +} +.accordian dt { + border-radius: 4px; + border: 1px solid #ccc; +} +.accordian dt.active { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} +.accordian dt, dd { + padding: 5px; +} +.accordian dt a { + color: black; + font-weight: bold; + display: block; +} +.accordian dt.active a { + color: #184E81; +} +.accordian dt:not(.active) a i { + display: none; +} +.accordian dd { + border-top: 0; + font-size: 12px; + margin-left: 0; + border: 1px solid #ccc; + border-top: none; + box-shadow: inset 0px 10px 5px -10px rgba(0,0,0,0.1); + background-color:rgba(42,103,172,0.1); +} +.accordian dt ~ dt { + margin-top: 5px; +} +.accordian dd:last-of-type { + position: relative; + top: -1px; +} diff --git a/scp/js/jquery.dropdown.js b/scp/js/jquery.dropdown.js index b885042086efee07eeb228c60abefd86ec278c0b..802a856c95e00041425a2be13178998b39de5bb7 100644 --- a/scp/js/jquery.dropdown.js +++ b/scp/js/jquery.dropdown.js @@ -37,7 +37,11 @@ if(jQuery) (function($) { var trigger = $(this), dropdown = $( $(this).attr('data-dropdown') ), isOpen = trigger.hasClass('dropdown-open'), - rtl = $('html').hasClass('rtl'); + rtl = $('html').hasClass('rtl'), + relative = trigger.offsetParent(), + offset = relative.offset(); + if (relative.get(0) !== document.body) + offset.top -= relative.scrollTop(); event.preventDefault(); event.stopPropagation(); @@ -50,9 +54,9 @@ if(jQuery) (function($) { dropdown.removeClass('anchor-right'); dropdown.css({ - left: dropdown.hasClass('anchor-right') ? - trigger.offset().left - (dropdown.outerWidth() - trigger.outerWidth() - 4) : trigger.offset().left, - top: trigger.offset().top + trigger.outerHeight() + left: -offset.left + (dropdown.hasClass('anchor-right') ? + trigger.offset().left - (dropdown.outerWidth() - trigger.outerWidth() - 4) : trigger.offset().left), + top: -offset.top + trigger.offset().top + trigger.outerHeight() }).show(); trigger.addClass('dropdown-open'); } diff --git a/scp/js/scp.js b/scp/js/scp.js index 6649ddeb2613a0d918895bfea4d0bff2cf378e37..5a93f7326fe8a19725388a39c0b0ed133ddea478 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -573,6 +573,11 @@ $.dialog = function (url, codes, cb, options) { $('div.body', $popup).empty(); if(cb) cb(xhr); } else { + try { + var json = $.parseJSON(resp); + if (json.redirect) return window.location.href = json.redirect; + } + catch (e) { } $('div.body', $popup).html(resp); $popup.effect('shake'); $('#msg_notice, #msg_error', $popup).delay(5000).slideUp(); diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql index 58aba311cac8c0317e8a1aae9e1c3ab1a615b417..f1062b3715b919389119bcb264f7f26918edd860 100644 --- a/setup/inc/streams/core/install-mysql.sql +++ b/setup/inc/streams/core/install-mysql.sql @@ -774,6 +774,20 @@ CREATE TABLE `%TABLE_PREFIX%plugin` ( primary key (`id`) ) DEFAULT CHARSET=utf8; +DROP TABLE IF EXISTS `%TABLE_PREFIX%queue`; +CREATE TABLE `%TABLE_PREFIX%queue` ( + `id` int(11) unsigned not null auto_increment, + `parent_id` int(11) unsigned not null default 0, + `flags` int(11) unsigned not null default 0, + `staff_id` int(11) unsigned not null default 0, + `sort` int(11) unsigned not null default 0, + `title` varchar(60), + `config` text, + `created` datetime not null, + `updated` datetime not null, + primary key (`id`) +) DEFAULT CHARSET=utf8; + DROP TABLE IF EXISTS `%TABLE_PREFIX%translation`; CREATE TABLE `%TABLE_PREFIX%translation` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT,