diff --git a/include/ajax.search.php b/include/ajax.search.php index 40cd7311050d761e2ad71d76cadbc295db290599..4edad32cac5b6c6acea5666cf5fc1426838940c9 100644 --- a/include/ajax.search.php +++ b/include/ajax.search.php @@ -231,7 +231,7 @@ class SearchAjaxAPI extends AjaxController { // TODO: Update queue columns (but without save) foreach ($_POST['columns'] as $colid) { - $col = QueueColumn::create(array("id" => $colid)); + $col = QueueColumn::create(array("id" => $colid, "queue" => $queue)); $col->update($_POST); $queue->addColumn($col); } @@ -248,8 +248,8 @@ class SearchAjaxAPI extends AjaxController { if (!$thisstaff) { Http::response(403, 'Agent login is required'); } - elseif (!isset($_GET['field'])) { - Http::response(400, '`field` parameter is required'); + elseif (!isset($_GET['field']) || !isset($_GET['id']) || !isset($_GET['colid'])) { + Http::response(400, '`field`, `id`, and `colid` parameters required'); } $fields = SavedSearch::getSearchableFields('Ticket'); if (!isset($fields[$_GET['field']])) { @@ -258,6 +258,10 @@ class SearchAjaxAPI extends AjaxController { } $field = $fields[$_GET['field']]; + // Ensure `name` is preserved + $field_name = $_GET['field']; + $id = $_GET['id']; + $column = QueueColumn::create(array('id' => $_GET['colid'])); $condition = new QueueColumnCondition(); include STAFFINC_DIR . 'templates/queue-column-condition.tmpl.php'; } @@ -268,11 +272,12 @@ class SearchAjaxAPI extends AjaxController { if (!$thisstaff) { Http::response(403, 'Agent login is required'); } - elseif (!isset($_GET['prop'])) { - Http::response(400, '`prop` parameter is required'); + elseif (!isset($_GET['prop']) || !isset($_GET['condition'])) { + Http::response(400, '`prop` and `condition` parameters required'); } $prop = $_GET['prop']; + $id = $_GET['condition']; include STAFFINC_DIR . 'templates/queue-column-condition-prop.tmpl.php'; } diff --git a/include/class.forms.php b/include/class.forms.php index 9171b5f90978ab2e4cd4d639ad2d08dffcba564e..71a5c717b778005487445aabfb2acb7192fa80a5 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -4213,7 +4213,7 @@ class VisibilityConstraint { else { @list($f, $op) = self::splitFieldAndOp($c); $field = $form->getField($f); - $wval = $field->getClean(); + $wval = $field ? $field->getClean() : null; switch ($op) { case 'eq': case null: diff --git a/include/class.orm.php b/include/class.orm.php index 3a51e8cdccffb525c949422f186c49b39b45baae..5c247029590f7bcddbb54d8867e33394ca7a9b20 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -822,7 +822,7 @@ class SqlExpr extends SqlFunction { $O = array(); foreach ($this->args as $field=>$value) { if ($value instanceof Q) { - $ex = $compiler->compileQ($value); + $ex = $compiler->compileQ($value, $model); $O[] = $ex->text; } else { @@ -833,7 +833,7 @@ class SqlExpr extends SqlFunction { $O[] = sprintf($op, $field, $compiler->input($value)); } } - return implode(' ', $O) . ($alias ? ' AS ' . $alias : ''); + return implode(' ', $O) . ($alias ? ' AS ' . $compiler->quote($alias) : ''); } } diff --git a/include/class.queue.php b/include/class.queue.php index c265952ad3465de3b84bd5eaa4d785fb08e4d9a1..d0aa1c64563d3c4ba32507bd385d843a8c055c94 100644 --- a/include/class.queue.php +++ b/include/class.queue.php @@ -284,10 +284,12 @@ extends ChoiceField { class QueueColumnCondition { var $config; + var $queue; var $properties = array(); - function __construct($config) { + function __construct($config, $queue=null) { $this->config = $config; + $this->queue = $queue; if (is_array($config['prop'])) $this->properties = $config['prop']; } @@ -296,17 +298,34 @@ class QueueColumnCondition { return $this->properties; } - function getField() { - } - // Add the annotation to a QuerySet function annotate($query) { - $criteria = $this->config['crit']; - $searchable = SavedSearch::getSearchableFields('Ticket'); + $Q = $this->getSearchQ(); - // Setup a dummy form with a source for field setup - $form = new Form($criteria); - $fields = array(); + // Add an annotation to the query + return $query->annotate(array( + $this->getAnnotationName() => new SqlExpr(array($Q)) + )); + } + + function getSearchQ() { + // FIXME + #$root = $this->getColumn()->getQueue()->getRoot(); + $root = 'Ticket'; + $searchable = SavedSearch::getSearchableFields($root); + list($name, $method, $value) = $this->config['crit']; + + // Lookup the field to search this condition + if (!isset($searchable[$name])) + return null; + $field = $searchable[$name]; + + // Fetch a criteria Q for the query + return $field->getSearchQ($method, $value, $name); + } + + static function isolateCriteria($criteria, $root='Ticket') { + $searchable = SavedSearch::getSearchableFields($root); foreach ($criteria as $k=>$v) { if (substr($k, -7) === '+method') { list($name,) = explode('+', $k, 2); @@ -317,55 +336,66 @@ class QueueColumnCondition { $field = $searchable[$name]; // Get the search method and value - $breakout = SavedSearch::getSearchField($field, $name); - $method = $breakout["{$name}+method"]; - $method->setForm($form); - if (!($method = $method->getClean())) - continue; + $method = $v; + // Not all search methods require a value + $value = $criteria["{$name}+{$method}"]; - if (!($value = $breakout["{$name}+{$method}"])) - continue; - - // Fetch a criteria Q for the query - $value = $value->getClean(); - $Q = $field->getSearchQ($method, $value, $name); - - // Add an annotation to the query - $query = $query->annotate(array( - $this->getAnnotationName() => new SqlExpr($Q) - )); - - // Only one field can be considered in the condition - break; + return array($name, $method, $value); } } - return $query; } function render($row, $text) { - $field = $this->getAnnotationName(); - if ($V = $row[$field]) { + $annotation = $this->getAnnotationName(); + if ($V = $row[$annotation]) { $style = array(); foreach ($this->getProperties() as $css=>$value) { - $style[] = "{$css}:{$value}"; + $field = QueueColumnConditionProperty::getField($css); + $field->value = $value; + $V = $field->getClean(); + if (is_array($V)) + $V = current($V); + $style[] = "{$css}:{$V}"; } $text = sprintf('<span style="%s">%s</span>', - implode(' ', $style), $text); + implode(';', $style), $text); } return $text; } function getAnnotationName() { - return 'howdy'; + // This should be predictable based on the criteria so that the + // query can deduplicate the same annotations used in different + // conditions + if (!isset($this->annotation_name)) { + $this->annotation_name = $this->getShortHash(); + } + return $this->annotation_name; } - static function fromJson($config) { + function __toString() { + list($name, $method, $value) = $this->config['crit']; + if (is_array($value)) + $value = implode('+', $value); + + return "{$name} {$method} {$value}"; + } + + function getHash($binary=false) { + return sha1($this->__toString(), $binary); + } + + function getShortHash() { + return substr($this->getHash(), -10); + } + + static function fromJson($config, $queue=null) { if (is_string($config)) - $config = JsonDataParser::decode($cnofig); + $config = JsonDataParser::decode($config); if (!is_array($config)) throw new BadMethodCallException('$config must be string or array'); - return new static($config); + return new static($config, $queue); } } @@ -399,17 +429,20 @@ extends ChoiceField { } static function getProperties() { - return array_keys(static::$properties[$this->property]); + return array_keys(static::$properties); } static function getField($prop) { $choices = static::$properties[$prop]; + if (!isset($choices)) + return null; if (is_array($choices)) return new ChoiceField(array( + 'name' => $prop, 'choices' => array_combine($choices, $choices), )); elseif (class_exists($choices)) - return new $choices(); + return new $choices(array('name' => $prop)); } function getChoices() { @@ -629,7 +662,7 @@ extends VerySimpleModel { // Do the decorations $this->_decorations = $this->decorations = array(); - foreach ($vars['decorations'] as $i=>$class) { + foreach (@$vars['decorations'] as $i=>$class) { if (!class_exists($class) || !is_subclass_of($class, 'QueueDecoration')) continue; if ($vars['deco_column'][$i] != $this->id) @@ -638,8 +671,54 @@ extends VerySimpleModel { $this->_decorations[] = QueueDecoration::fromJson($json); $this->decorations[] = $json; } + + // Do the conditions + $this->_conditions = $this->conditions = array(); + foreach (@$vars['conditions'] as $i=>$id) { + if ($vars['condition_column'][$i] != $this->id) + // Not a condition for this column + continue; + // Determine the criteria + $name = $vars['condition_field'][$i]; + $fields = SavedSearch::getSearchableFields($this->getQueue()->getRoot()); + if (!isset($fields[$name])) + // No such field exists for this queue root type + continue; + $field = $fields[$name]; + $parts = SavedSearch::getSearchField($field, $name); + $search_form = new SimpleForm($parts, $vars, array('id' => $id)); + $search_form->getField("{$name}+search")->value = true; + $crit = $search_form->getClean(); + // Check the box to enable searching on the field + $crit["{$name}+search"] = true; + + // Convert search criteria to a Q instance + $crit = QueueColumnCondition::isolateCriteria($crit); + + // Determine the properties + $props = array(); + foreach ($vars['properties'] as $i=>$cid) { + if ($cid != $id) + // Not a property for this condition + continue; + + // Determine the property configuration + $prop = $vars['property_name'][$i]; + if (!($F = QueueColumnConditionProperty::getField($prop))) { + // Not a valid property + continue; + } + $prop_form = new SimpleForm(array($F), $vars, array('id' => $cid)); + $props[$prop] = $prop_form->getClean(); + } + $json = array('crit' => $crit, 'prop' => $props); + $this->_conditions[] = QueueColumnCondition::fromJson($json); + $this->conditions[] = $json; + } + // Store as JSON array $this->decorations = JsonDataEncoder::encode($this->decorations); + $this->conditions = JsonDataEncoder::encode($this->conditions); } } diff --git a/include/staff/templates/queue-column-condition-prop.tmpl.php b/include/staff/templates/queue-column-condition-prop.tmpl.php index f29f5947d99b6a57f73df9aa96648b261cb0b0af..be7a820a4efc9fc60c5073e612a8a35db3a7d9bc 100644 --- a/include/staff/templates/queue-column-condition-prop.tmpl.php +++ b/include/staff/templates/queue-column-condition-prop.tmpl.php @@ -2,12 +2,14 @@ /** * Calling conventions * - * $name - condition field name, like 'thread__lastmessage' + * $id - temporary condition ID number for the property * $prop - CSS property name from QueueColumnConditionProperty::$properties * $v - value for the property */ ?> <div class="condition-property"> + <input type="hidden" name="properties[]" value="<?php echo $id; ?>" /> + <input type="hidden" name="property_name[]" value="<?php echo $prop; ?>" /> <div class="pull-right"> <a href="#" onclick="javascript:$(this).closest('.condition-property').remove()" ><i class="icon-trash"></i></a> @@ -15,9 +17,8 @@ <div><?php echo mb_convert_case($prop, MB_CASE_TITLE); ?></div> <?php $F = QueueColumnConditionProperty::getField($prop); - $F->set('name', "prop-{$name}-{$prop}"); $F->value = $v; - $form = new SimpleForm(array($F), $_POST); + $form = new SimpleForm(array($F), false, array('id' => $id)); echo $F->render(); echo $form->getMedia(); ?> diff --git a/include/staff/templates/queue-column-condition.tmpl.php b/include/staff/templates/queue-column-condition.tmpl.php index d445f4f4d1ac567018b55fc414b876673c5a605f..1c1684b43b62491d2e7e92b5e813d45f2d808d97 100644 --- a/include/staff/templates/queue-column-condition.tmpl.php +++ b/include/staff/templates/queue-column-condition.tmpl.php @@ -4,8 +4,15 @@ // $field - field for the condition (Ticket / Last Update) // $properties - currently-configured properties for the condition // $condition - <QueueColumnCondition> instance for this condition +// $column - <QueueColumn> to which the condition belongs +// $id - temporary ID number for the condition +// $field_name - search path / name for the field ?> <div class="condition"> + <input name="conditions[]" value="<?php echo $id; ?>" type="hidden" /> + <input name="condition_column[]" value="<?php echo $column->getId(); ?>" + type="hidden" /> + <input name="condition_field[]" value="<?php echo $field_name; ?>" type="hidden" /> <div class="pull-right"> <a href="#" onclick="javascript: $(this).closest('.condition').remove(); "><i class="icon-trash"></i></a> @@ -13,16 +20,15 @@ <?php echo $field->get('label'); ?> <div class="advanced-search"> <?php -$name = $field->get('name'); -$parts = SavedSearch::getSearchField($field, $name); +$parts = SavedSearch::getSearchField($field, $field_name); // Drop the search checkbox field -unset($parts["{$name}+search"]); +unset($parts["{$field_name}+search"]); foreach ($parts as $name=>$F) { if (substr($name, -7) == '+method') // XXX: Hack unset($F->ht['visibility']); } -$form = new SimpleForm($parts); +$form = new SimpleForm($parts, false, array('id' => $id)); foreach ($form->getFields() as $F) { ?> <fieldset id="field<?php echo $F->getWidget()->id; ?>" <?php @@ -44,7 +50,8 @@ foreach ($form->getFields() as $F) { ?> <?php } ?> <div class="properties" style="margin-left: 25px; margin-top: 10px"> -<?php foreach ($condition->getProperties() as $prop=>$v) { +<?php +foreach ($condition->getProperties() as $prop=>$v) { include 'queue-column-condition-prop.tmpl.php'; } ?> <div style="margin-top: 10px"> @@ -55,7 +62,7 @@ foreach ($form->getFields() as $F) { ?> container = $this.closest('.properties'); $.ajax({ url: 'ajax.php/queue/condition/addProperty', - data: { prop: selected.val() }, + data: { prop: selected.val(), condition: <?php echo $id; ?> }, dataType: 'html', success: function(html) { $(html).insertBefore(container); diff --git a/include/staff/templates/queue-column.tmpl.php b/include/staff/templates/queue-column.tmpl.php index 925687ea345cf063a867f4eecb28b6ee2afa93df..5d2f23e6f57c6ef5c5ecfbe39c8230049f7e6400 100644 --- a/include/staff/templates/queue-column.tmpl.php +++ b/include/staff/templates/queue-column.tmpl.php @@ -113,18 +113,7 @@ $data_form = $column->getDataConfigForm($_POST); } ?> <div style="margin-top: 20px"> <i class="icon-plus-sign"></i> - <select id="add-condition" onchange="javascript: - var $this = $(this), - container = $this.closest('div'); - $.ajax({ - url: 'ajax.php/queue/condition/add', - data: { field: $(this).find(':selected').val() }, - dataType: 'html', - success: function(html) { - $(html).insertBefore(container); - } - }); - "> + <select class="add-condition"> <option>— <?php echo __("Add a condition"); ?> —</option> <?php foreach (SavedSearch::getSearchableFields('Ticket') as $path=>$f) { @@ -132,6 +121,26 @@ $data_form = $column->getDataConfigForm($_POST); } ?> </select> + <script> + $(function() { + var colid = <?php echo $colid ?: 0 ?>, + nextid = colid * 40; + $('#' + colid + '-conditions select.add-condition').change(function() { + var $this = $(this), + container = $this.closest('div'), + selected = $this.find(':selected'); + $.ajax({ + url: 'ajax.php/queue/condition/add', + data: { field: selected.val(), colid: colid, id: nextid }, + dataType: 'html', + success: function(html) { + $(html).insertBefore(container); + nextid++; + } + }); + }); + }); + </script> </div> </div> </div>