diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index 3d93e9a9cf3585d38189e8c705dafe54246fa2aa..7d0ee9eb7f5e9c28aa7aa54dea466228946046db 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -208,13 +208,16 @@ class TicketsAjaxAPI extends AjaxController { $cdata_search = false; foreach (TicketForm::getInstance()->getFields() as $f) { if (isset($req[$f->getFormName()]) - && ($val = $req[$f->getFormName()]) - && strlen(trim($val))) { + && ($val = $req[$f->getFormName()])) { $name = $f->get('name') ? $f->get('name') : 'field_'.$f->get('id'); - if ($f->getImpl()->hasIdValue() && is_numeric($val)) { - $cwhere = "cdata.`{$name}_id` = ".db_input($val); - $criteria["cdata.{$name}_id"] = $val; + if (is_array($val)) { + $cwhere = '(' . implode(' OR ', array_map( + function($k) use ($name) { + return sprintf('FIND_IN_SET(%s, `%s`)', db_input($k), $name); + }, $val) + ) . ')'; + $criteria["cdata.{$name}"] = $val; } else { $cwhere = "cdata.`$name` LIKE '%".db_real_escape($val)."%'"; @@ -262,8 +265,8 @@ class TicketsAjaxAPI extends AjaxController { $uid = md5($_SERVER['QUERY_STRING']); $_SESSION["adv_$uid"] = $tickets; $result['success'] = sprintf(__("Search criteria matched %s"), - sprintf(_N('%d ticket', '%d tickets'), count($tickets), - $tickets)) + sprintf(_N('%d ticket', '%d tickets', count($tickets)), count($tickets) + )) . " - <a href='tickets.php?advsid=$uid'>".__('view')."</a>"; } else { $result['fail']=__('No tickets found matching your search criteria.'); diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php index 5a2ecec0b2ea62290152a9d755253cd00cf4f290..c5133c9d6a40e81df5c47406c13e6fe38565f617 100644 --- a/include/class.dynamic_forms.php +++ b/include/class.dynamic_forms.php @@ -196,16 +196,19 @@ class DynamicForm extends VerySimpleModel { if (!$impl->hasData() || $impl->isPresentationOnly()) continue; + $id = $f->get('id'); $name = ($f->get('name')) ? $f->get('name') - : 'field_'.$f->get('id'); + : 'field_'.$id; - $fields[] = sprintf( - 'MAX(IF(field.name=\'%1$s\',ans.value,NULL)) as `%1$s`', - $name); - if ($impl->hasIdValue()) { + if ($impl instanceof ChoiceField || $impl instanceof SelectionField) { $fields[] = sprintf( - 'MAX(IF(field.name=\'%1$s\',ans.value_id,NULL)) as `%1$s_id`', - $name); + 'MAX(CASE WHEN field.id=\'%1$s\' THEN REPLACE(REPLACE(REPLACE(REPLACE(coalesce(ans.value_id, ans.value), \'{\', \'\'), \'}\', \'\'), \'"\', \'\'), \':\', \',\') ELSE NULL END) as `%2$s`', + $id, $name); + } + else { + $fields[] = sprintf( + 'MAX(IF(field.id=\'%1$s\',coalesce(ans.value_id, ans.value),NULL)) as `%2$s`', + $id, $name); } } return $fields; @@ -249,8 +252,8 @@ Filter::addSupportedMatches(/* @trans */ 'User Data', function() { if (!$f->hasData()) continue; $matches['field.'.$f->get('id')] = __('User').' / '.$f->getLabel(); - if (($fi = $f->getImpl()) instanceof SelectionField) { - foreach ($fi->getList()->getForm()->getFields() as $p) { + if (($fi = $f->getImpl()) && $fi->hasSubFields()) { + foreach ($fi->getSubFields() as $p) { $matches['field.'.$f->get('id').'.'.$p->get('id')] = __('User').' / '.$f->getLabel().' / '.$p->getLabel(); } @@ -321,10 +324,8 @@ class TicketForm extends DynamicForm { $f = $answer->getField(); $name = $f->get('name') ? $f->get('name') : 'field_'.$f->get('id'); - $ids = $f->hasIdValue(); - $fields = sprintf('`%s`=', $name) . db_input($answer->get('value')); - if ($f->hasIdValue()) - $fields .= sprintf(',`%s_id`=', $name) . db_input($answer->getIdValue()); + $fields = sprintf('`%s`=', $name) . db_input( + implode(',', $answer->getSearchKeys())); $sql = 'INSERT INTO `'.TABLE_PREFIX.'ticket__cdata` SET '.$fields .', `ticket_id`='.db_input($answer->getEntry()->get('object_id')) .' ON DUPLICATE KEY UPDATE '.$fields; @@ -339,8 +340,8 @@ Filter::addSupportedMatches(/* @trans */ 'Ticket Data', function() { if (!$f->hasData()) continue; $matches['field.'.$f->get('id')] = __('Ticket').' / '.$f->getLabel(); - if (($fi = $f->getImpl()) instanceof SelectionField) { - foreach ($fi->getList()->getForm()->getFields() as $p) { + if (($fi = $f->getImpl()) && $fi->hasSubFields()) { + foreach ($fi->getSubFields() as $p) { $matches['field.'.$f->get('id').'.'.$p->get('id')] = __('Ticket').' / '.$f->getLabel().' / '.$p->getLabel(); } @@ -381,8 +382,8 @@ Filter::addSupportedMatches(/* trans */ 'Custom Forms', function() { if (!$f->hasData()) continue; $matches['field.'.$f->get('id')] = $form->getTitle().' / '.$f->getLabel(); - if (($fi = $f->getImpl()) instanceof SelectionField) { - foreach ($fi->getList()->getProperties() as $p) { + if (($fi = $f->getImpl()) && $fi->hasSubFields()) { + foreach ($fi->getSubFields() as $p) { $matches['field.'.$f->get('id').'.'.$p->get('id')] = $form->getTitle().' / '.$f->getLabel().' / '.$p->getLabel(); } @@ -448,7 +449,7 @@ class DynamicFormField extends VerySimpleModel { */ function setConfiguration(&$errors=array()) { $config = array(); - foreach ($this->getConfigurationForm() as $name=>$field) { + foreach ($this->getConfigurationForm($_POST)->getFields() as $name=>$field) { $config[$name] = $field->to_php($field->getClean()); $errors = array_merge($errors, $field->errors()); } @@ -944,6 +945,14 @@ class DynamicFormEntryAnswer extends VerySimpleModel { ); } + function getSearchKeys() { + $val = $this->getField()->to_php($this->getValue()); + if (is_array($val)) + return array_keys($val); + + return [$val]; + } + function asVar() { return (is_object($this->getValue())) ? $this->getValue() : $this->toString(); @@ -1004,15 +1013,13 @@ class SelectionField extends FormField { } function to_database($value) { - $id = null; if (is_array($value)) { reset($value); - $id = key($value); } if ($value && is_array($value)) $value = JsonDataEncoder::encode($value); - return array($value, $id); + return $value; } function to_php($value, $id=false) { @@ -1023,12 +1030,17 @@ class SelectionField extends FormField { if (isset($choices[$value])) $value = $choices[$value]; } + // Don't set the ID here as multiselect prevents using exactly one + // ID value. Instead, stick with the JSON value only. return $value; } - function hasIdValue() { + function hasSubFields() { return true; } + function getSubFields() { + return $this->getConfigurationForm()->getFields(); + } function toString($items) { return ($items && is_array($items)) diff --git a/include/class.forms.php b/include/class.forms.php index aa9405d80d056e0e8764c0b330d7469e14426798..040cd255425d55c5a24140bbda9da0cffa463606 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -28,8 +28,11 @@ class Form { function __construct($fields=array(), $source=null, $options=array()) { $this->fields = $fields; - foreach ($fields as $f) + foreach ($fields as $k=>$f) { $f->setForm($this); + if (!$f->get('name') && $k) + $f->set('name', $k); + } if (isset($options['title'])) $this->title = $options['title']; if (isset($options['instructions'])) @@ -535,12 +538,44 @@ class FormField { return false; } - function getConfigurationForm() { + /** + * Indicates if the field has subfields accessible via getSubFields() + * method. Useful for filter integration. Should connect with + * getFilterData() + */ + function hasSubFields() { + return false; + } + function getSubFields() { + return null; + } + + /** + * Indicates if the field provides for searching for something other + * than keywords. For instance, textbox fields can have hits by keyword + * searches alone, but selection fields should provide the option to + * match a specific value or set of values and therefore need to + * participate on any search builder. + */ + function hasSpecialSearch() { + return true; + } + + function getConfigurationForm($source=null) { if (!$this->_cform) { $type = static::getFieldType($this->get('type')); $clazz = $type[1]; $T = new $clazz(); - $this->_cform = $T->getConfigurationOptions(); + $config = $this->getConfiguration(); + $this->_cform = new Form($T->getConfigurationOptions(), $source); + if (!$source && $config) { + foreach ($this->_cform->getFields() as $name=>$f) { + if (isset($config[$name])) + $f->value = $config[$name]; + elseif ($f->get('default')) + $f->value = $f->get('default'); + } + } } return $this->_cform; } @@ -597,6 +632,10 @@ class TextboxField extends FormField { ); } + function hasSpecialSearch() { + return false; + } + function validateEntry($value) { parent::validateEntry($value); $config = $this->getConfiguration(); @@ -661,6 +700,10 @@ class TextareaField extends FormField { ); } + function hasSpecialSearch() { + return false; + } + function display($value) { $config = $this->getConfiguration(); if ($config['html']) @@ -705,6 +748,10 @@ class PhoneField extends FormField { ); } + function hasSpecialSearch() { + return false; + } + function validateEntry($value) { parent::validateEntry($value); $config = $this->getConfiguration(); @@ -990,6 +1037,9 @@ class ThreadEntryField extends FormField { function isPresentationOnly() { return true; } + function hasSpecialSearch() { + return false; + } function getConfigurationOptions() { global $cfg; @@ -1302,6 +1352,10 @@ class FileUploadField extends FormField { ); } + function hasSpecialSearch() { + return false; + } + /** * Called from the ajax handler for async uploads via web clients. */ @@ -1634,7 +1688,7 @@ class ChoicesWidget extends Widget { function render($mode=false) { - if ($mode && $mode == 'view') { + if ($mode == 'view') { if (!($val = (string) $this->field)) $val = sprintf('<span class="faded">%s</span>', __('None')); @@ -1643,6 +1697,10 @@ class ChoicesWidget extends Widget { } $config = $this->field->getConfiguration(); + if ($mode == 'search') { + $config['multiselect'] = true; + } + // Determine the value for the default (the one listed if nothing is // selected) $choices = $this->field->getChoices(true); diff --git a/include/class.list.php b/include/class.list.php index b4423c3aab51662afb6cb316582607ed81f3b50b..98be809500197f76c0265086236ee4b665ad4adb 100644 --- a/include/class.list.php +++ b/include/class.list.php @@ -66,7 +66,7 @@ interface CustomListItem { function getSortOrder(); function getConfiguration(); - function getConfigurationForm(); + function getConfigurationForm($source=null); function isEnabled(); @@ -538,9 +538,8 @@ class DynamicListItem extends VerySimpleModel implements CustomListItem { function setConfiguration(&$errors=array()) { $config = array(); - foreach ($this->getConfigurationForm()->getFields() as $field) { - $val = $field->to_database($field->getClean()); - $config[$field->get('id')] = is_array($val) ? $val[1] : $val; + foreach ($this->getConfigurationForm($_POST)->getFields() as $field) { + $config[$field->get('id')] = $field->to_php($field->getClean()); $errors = array_merge($errors, $field->errors()); } if (count($errors) === 0) @@ -549,10 +548,20 @@ class DynamicListItem extends VerySimpleModel implements CustomListItem { return count($errors) === 0; } - function getConfigurationForm() { + function getConfigurationForm($source=null) { if (!$this->_form) { + $config = $this->getConfiguration(); $this->_form = DynamicForm::lookup( - array('type'=>'L'.$this->get('list_id'))); + array('type'=>'L'.$this->get('list_id')))->getForm($source); + if (!$source && $config) { + foreach ($this->_form->getFields() as $f) { + $name = $f->get('id'); + if (isset($config[$name])) + $f->value = $f->to_php($config[$name]); + else if ($f->get('default')) + $f->value = $f->get('default'); + } + } } return $this->_form; } @@ -788,19 +797,31 @@ class TicketStatus extends VerySimpleModel implements CustomListItem { } function getForm() { - return $this->getConfigurationForm(); - } - - function getConfigurationForm() { - if (!$this->_form && $this->_list) { $this->_form = DynamicForm::lookup( - array('type'=>'L'.$this->_list->getId())); + array('type'=>'L'.$this->_list->getId())); } - return $this->_form; } + function getConfigurationForm($source=null) { + + if ($form = $this->getForm()) { + $config = $this->getConfiguration(); + $form = $form->getForm($source); + if (!$source && $config) { + foreach ($form->getFields() as $f) { + $name = $f->get('id'); + if (isset($config[$name])) + $f->value = $f->to_php($config[$name]); + else if ($f->get('default')) + $f->value = $f->get('default'); + } + } + } + return $form; + } + function isEnabled() { return $this->hasFlag('mode', self::ENABLED); } @@ -896,8 +917,8 @@ class TicketStatus extends VerySimpleModel implements CustomListItem { if (!$this->_settings) $this->_settings = array(); - if ($this->getConfigurationForm()) { - foreach ($this->getConfigurationForm()->getFields() as $f) { + if ($this->getForm()) { + foreach ($this->getForm()->getFields() as $f) { $name = mb_strtolower($f->get('name')); $id = $f->get('id'); switch($name) { @@ -922,7 +943,7 @@ class TicketStatus extends VerySimpleModel implements CustomListItem { function setConfiguration(&$errors=array()) { $properties = array(); - foreach ($this->getConfigurationForm()->getFields() as $f) { + foreach ($this->getConfigurationForm($_POST)->getFields() as $f) { if ($this->isInternal() //Item is internal. && !$f->isEditable()) continue; diff --git a/include/class.search.php b/include/class.search.php index 270e0322055ae05714954431ae8c7a812ff2bcf2..9e3a380efbfc081c42fe27ddf5b32a95f7f244d4 100644 --- a/include/class.search.php +++ b/include/class.search.php @@ -331,7 +331,17 @@ class MysqlSearchBackend extends SearchBackend { // Search ticket CDATA table $cdata_search = true; $name = substr($name, 6); - $where[] = sprintf("cdata.%s = %s", $name, db_input($value)); + if (is_array($value)) { + $where[] = '(' . implode(' OR ', array_map( + function($k) use ($name) { + return sprintf('FIND_IN_SET(%s, cdata.`%s`)', + db_input($k), $name); + }, $value) + ) . ')'; + } + else { + $where[] = sprintf("cdata.%s = %s", $name, db_input($value)); + } } } } diff --git a/include/staff/templates/dynamic-field-config.tmpl.php b/include/staff/templates/dynamic-field-config.tmpl.php index da2621b3fef930186fe68339b3dbea4b974b9617..af257465289439f8748ffc948911be5fc62e2f1c 100644 --- a/include/staff/templates/dynamic-field-config.tmpl.php +++ b/include/staff/templates/dynamic-field-config.tmpl.php @@ -5,15 +5,9 @@ echo $field->get('id'); ?>"> <?php echo csrf_token(); - $config = $field->getConfiguration(); - $form = new Form($field->getConfigurationForm()); + $form = $field->getConfigurationForm(); echo $form->getMedia(); - foreach ($form->getFields() as $name=>$f) { - if (isset($config[$name])) - $f->value = $config[$name]; - else if ($f->get('default')) - $f->value = $f->get('default'); - ?> + foreach ($form->getFields() as $name=>$f) { ?> <div class="flush-left custom-field"> <div class="field-label"> <label for="<?php echo $f->getWidget()->name; ?>"> diff --git a/include/staff/templates/list-item-properties.tmpl.php b/include/staff/templates/list-item-properties.tmpl.php index 44ce0de055728a163cd805e59ec99a5a8e549fd9..51e26b65a55a3a205011cef58951d11280875619 100644 --- a/include/staff/templates/list-item-properties.tmpl.php +++ b/include/staff/templates/list-item-properties.tmpl.php @@ -8,13 +8,9 @@ echo csrf_token(); $config = $item->getConfiguration(); $internal = $item->isInternal(); - foreach ($item->getConfigurationForm()->getFields() as $f) { - $name = $f->get('id'); - if (isset($config[$name])) - $f->value = $f->to_php($config[$name]); - else if ($f->get('default')) - $f->value = $f->get('default'); - ?> + $form = $item->getConfigurationForm(); + echo $form->getMedia(); + foreach ($form->getFields() as $f) { ?> <div class="custom-field"> <div class="field-label"> <label for="<?php echo $f->getWidget()->name; ?>" diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php index 3f828d210662a13a70264c9405069e991b7fe252..671600b785e20d90df39173482da78c8a5803d9f 100644 --- a/include/staff/tickets.inc.php +++ b/include/staff/tickets.inc.php @@ -255,7 +255,7 @@ $qselect.=' ,IF(ticket.duedate IS NULL,IF(sla.id IS NULL, NULL, DATE_ADD(ticket. .' ,ticket.created as ticket_created, CONCAT_WS(" ", staff.firstname, staff.lastname) as staff, team.name as team ' .' ,IF(staff.staff_id IS NULL,team.name,CONCAT_WS(" ", staff.lastname, staff.firstname)) as assigned ' .' ,IF(ptopic.topic_pid IS NULL, topic.topic, CONCAT_WS(" / ", ptopic.topic, topic.topic)) as helptopic ' - .' ,cdata.priority_id, cdata.subject, pri.priority_desc, pri.priority_color'; + .' ,cdata.priority as priority_id, cdata.subject, pri.priority_desc, pri.priority_color'; $qfrom.=' LEFT JOIN '.TICKET_LOCK_TABLE.' tlock ON (ticket.ticket_id=tlock.ticket_id AND tlock.expire>NOW() AND tlock.staff_id!='.db_input($thisstaff->getId()).') ' @@ -265,7 +265,7 @@ $qfrom.=' LEFT JOIN '.TICKET_LOCK_TABLE.' tlock ON (ticket.ticket_id=tlock.ticke .' LEFT JOIN '.TOPIC_TABLE.' topic ON (ticket.topic_id=topic.topic_id) ' .' LEFT JOIN '.TOPIC_TABLE.' ptopic ON (ptopic.topic_id=topic.topic_pid) ' .' LEFT JOIN '.TABLE_PREFIX.'ticket__cdata cdata ON (cdata.ticket_id = ticket.ticket_id) ' - .' LEFT JOIN '.PRIORITY_TABLE.' pri ON (pri.priority_id = cdata.priority_id)'; + .' LEFT JOIN '.PRIORITY_TABLE.' pri ON (pri.priority_id = cdata.priority)'; TicketForm::ensureDynamicDataView(); @@ -648,9 +648,9 @@ if ($results) { $tform = TicketForm::objects()->one(); echo $tform->getForm()->getMedia(); foreach ($tform->getInstance()->getFields() as $f) { - if (in_array($f->get('type'), array('text', 'memo', 'phone', 'thread'))) + if (!$f->hasData()) continue; - elseif (!$f->hasData()) + elseif (!$f->getImpl()->hasSpecialSearch()) continue; ?><fieldset class="span6"> <label><?php echo $f->getLabel(); ?>:</label><div><?php diff --git a/include/upgrader/streams/core/8f99b8bf-03ff59bf.cleanup.sql b/include/upgrader/streams/core/8f99b8bf-03ff59bf.cleanup.sql index bccb432fbf33b17fc27d54fc90490fcfb01e7315..dbeb1fa39788d22ebe82ba7ef6c559d2de363bcf 100644 --- a/include/upgrader/streams/core/8f99b8bf-03ff59bf.cleanup.sql +++ b/include/upgrader/streams/core/8f99b8bf-03ff59bf.cleanup.sql @@ -4,4 +4,7 @@ DELETE FROM `%TABLE_PREFIX%config` ALTER TABLE `%TABLE_PREFIX%ticket` DROP COLUMN `status`; +-- Regenerate the CDATA table with the new format for 1.9.4 +DROP TABLE `%TABLE_PREFIX%ticket__cdata`; + OPTIMIZE TABLE `%TABLE_PREFIX%ticket`;