From dd1d66384d941a80058ed96200dc1ed2ba673b11 Mon Sep 17 00:00:00 2001
From: Peter Rotich <peter@osticket.com>
Date: Wed, 9 Jul 2014 20:05:38 +0000
Subject: [PATCH] Add multiselect support

Add multiselect  option to selection and choices fields.
---
 include/class.dynamic_forms.php | 155 +++++++++++++++++++++-----------
 include/class.forms.php         |  52 ++++++-----
 include/class.list.php          |   6 +-
 3 files changed, 135 insertions(+), 78 deletions(-)

diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php
index 2d685db89..f097a2e4d 100644
--- a/include/class.dynamic_forms.php
+++ b/include/class.dynamic_forms.php
@@ -920,66 +920,90 @@ class SelectionField extends FormField {
     function getList() {
         if (!$this->_list)
             $this->_list = DynamicList::lookup($this->getListId());
+
         return $this->_list;
     }
 
     function parse($value) {
+
+        if (!($list=$this->getList()))
+            return null;
+
         $config = $this->getConfiguration();
-        if (is_int($value))
-            return $this->to_php($this->getWidget()->getEnteredValue(), (int) $value);
-        elseif (!$config['typeahead'])
-            return $this->to_php(null, (int) $value);
-        else
-            return $this->to_php($value);
+        $choices = $this->getChoices();
+        $selection = array();
+        if ($config['typeahead']) {
+            // Entered value
+            $val = $this->getWidget()->getEnteredValue();
+            if (($i=$list->getItem($val)) && $i->getId() == $value)
+                $selection[$i->getId()] = $i->getValue();
+            elseif ($val && isset($choices[$value])) //perhaps old deleted item...
+                $selection[$value] = $choices[$value];
+        } elseif ($value && is_array($value)) {
+            foreach ($value as $v) {
+                if (($i=$list->getItem((int) $v)))
+                    $selection[$i->getId()] = $i->getValue();
+                elseif (isset($choices[$v]))
+                    $selection[$v] = $choices[$v];
+            }
+        }
+
+        return $selection;
+    }
+
+    function to_database($value) {
+        if ($value && is_array($value))
+            $value = JsonDataEncoder::encode($value);
+
+        return $value;
     }
 
     function to_php($value, $id=false) {
-        if ($value === null && $id === null)
-            return null;
-        if ($id && is_int($id))
-            $item = DynamicListItem::lookup($id);
-        # Attempt item lookup by name too
-        if (!$item || ($value !== null && $value != $item->get('value'))) {
-            $item = DynamicListItem::lookup(array(
-                'value'=>$value,
-                'list_id'=>$this->getListId()));
-        }
-        return ($item) ? $item : $value;
+        return ($value && !is_array($value))
+            ? JsonDataParser::parse($value) : $value;
     }
 
     function hasIdValue() {
         return true;
     }
 
-    function to_database($item) {
-        if ($item instanceof DynamicListItem)
-            return array($item->value, $item->id);
-        return null;
-    }
-
-    function toString($item) {
-        return ($item instanceof DynamicListItem)
-            ? $item->toString() : (string) $item;
+    function toString($items) {
+        return ($items && is_array($items))
+            ? explode(', ', $items) : (string) $items;
     }
 
-    function validateEntry($item) {
-        $config = $this->getConfiguration();
-        parent::validateEntry($item);
-        if ($item && !$item instanceof DynamicListItem)
-            $this->_errors[] = 'Select a value from the list';
-        elseif ($item && $config['typeahead']
-                && $this->getWidget()->getEnteredValue() != $item->get('value'))
-            $this->_errors[] = 'Select a value from the list';
+    function validateEntry($entry) {
+        parent::validateEntry($entry);
+        if (!$this->errors()) {
+            $config = $this->getConfiguration();
+            if (!$entry || count($entry) == 0)
+                $this->_errors[] = 'Select a value from the list';
+            elseif ($config['typeahead']
+                    && !in_array($this->getWidget()->getEnteredValue(), $entry))
+                $this->_errors[] = 'Select a value from the list';
+        }
     }
 
     function getConfigurationOptions() {
         return array(
-            'typeahead' => new ChoiceField(array(
+            'widget' => new ChoiceField(array(
                 'id'=>1, 'label'=>'Widget', 'required'=>false,
-                'default'=>false,
-                'choices'=>array(false=>'Drop Down', true=>'Typeahead'),
+                'default' => 'dropdown',
+                'choices'=>array(
+                    'dropdown' => 'Drop Down',
+                    'typeahead' =>'Typeahead',
+                ),
+                'configuration'=>array(
+                    'multiselect' => false,
+                ),
                 'hint'=>'Typeahead will work better for large lists'
             )),
+            'multiselect' => new BooleanField(array(
+                'id'=>1, 'label'=>'Multiselect', 'required'=>false, 'default'=>false,
+                'configuration'=>array(
+                    'desc'=>'Allow multiple selections'),
+                'hint' => 'Dropdown only',
+            )),
             'prompt' => new TextboxField(array(
                 'id'=>2, 'label'=>'Prompt', 'required'=>false, 'default'=>'',
                 'hint'=>'Leading text shown before a value is selected',
@@ -988,14 +1012,34 @@ class SelectionField extends FormField {
         );
     }
 
-    function getChoices() {
-        if (!$this->_choices) {
+    function getConfiguration() {
+
+        $config = parent::getConfiguration();
+        if ($config['widget'])
+            $config['typeahead'] = isset($config['widget']['typeahead']);
+
+        //Typeahed doesn't support multiselect for now  TODO: Add!
+        if ($config['typeahead'])
+            $config['multiselect'] = false;
+
+        return $config;
+    }
+
+    function getChoices($verbose=false) {
+        if (!$this->_choices || $verbose) {
             $this->_choices = array();
             foreach ($this->getList()->getItems() as $i)
-                $this->_choices[$i->get('id')] = $i->get('value');
-            if ($this->value && !isset($this->_choices[$this->value])) {
-                $v = DynamicListItem::lookup($this->value);
-                $this->_choices[$v->get('id')] = $v->get('value').' (Disabled)';
+                $this->_choices[$i->getId()] = $i->getValue();
+
+            // Retired old selections
+            $values = ($a=$this->getAnswer()) ? $a->getValue() : array();
+            if ($values && is_array($values)) {
+                foreach ($values as $k => $v) {
+                    if (!isset($this->_choices[$k])) {
+                        if ($verbose) $v .= ' (retired)';
+                        $this->_choices[$k] = $v;
+                    }
+                }
             }
         }
         return $this->_choices;
@@ -1019,27 +1063,30 @@ class SelectionField extends FormField {
 
 class SelectionWidget extends ChoicesWidget {
     function render($mode=false) {
+
         $config = $this->field->getConfiguration();
-        $value = false;
-        if ($this->value instanceof DynamicListItem) {
-            // Loaded from database
-            $value = $this->value->get('id');
-            $name = $this->value->get('value');
-        } elseif ($this->value) {
-            // Loaded from POST
+        if (($value=$this->getValue()))
+            $value =  $this->field->parse($value);
+        elseif ($this->value)
             $value = $this->value;
-            $name = $this->getEnteredValue();
-        }
+
         if (!$config['typeahead'] || $mode=='search') {
             $this->value = $value;
             return parent::render($mode);
         }
 
+        if ($value && is_array($value)) {
+            $name = current($value);
+            $value = key($value);
+        }
+
         $source = array();
         foreach ($this->field->getList()->getItems() as $i)
             $source[] = array(
-                'value' => $i->get('value'), 'id' => $i->get('id'),
-                'info' => $i->get('value')." -- ".$i->get('extra'),
+                'value' => $i->getValue(), 'id' => $i->getId(),
+                'info' => sprintf('%s %s',
+                    $i->getValue(),
+                    (($extra= $i->getAbbrev()) ? "-- $extra" : '')),
             );
         ?>
         <span style="display:inline-block">
diff --git a/include/class.forms.php b/include/class.forms.php
index c5cb405a6..246aa0169 100644
--- a/include/class.forms.php
+++ b/include/class.forms.php
@@ -751,9 +751,9 @@ class ChoiceField extends FormField {
         if (is_array($value)) {
             foreach($value as $k => $v)
                 if (isset($choices[$v]))
-                    $values[] = $v;
+                    $values[$v] = $choices[$v];
         } elseif(isset($choices[$value])) {
-            $values[] = $value;
+            $values[$value] = $choices[$value];
         }
 
         return $values ?: null;
@@ -761,13 +761,14 @@ class ChoiceField extends FormField {
 
     function to_database($value) {
         if ($value && is_array($value))
-            $value = implode(',', $value);
+            $value = JsonDataEncoder::encode($value);
 
         return $value;
     }
 
     function to_php($value) {
-        return $value ? explode(',', $value) : $value;
+        return ($value && !is_array($value))
+            ? JsonDataParser::parse($value) : $value;
     }
 
     function toString($value) {
@@ -775,9 +776,7 @@ class ChoiceField extends FormField {
         $choices = $this->getChoices();
         $selection = array();
         if ($value && is_array($value)) {
-            foreach ($value as $v)
-                if (isset($choices[$v]))
-                    $selection[] = $choices[$v];
+            $selection = $value;
         } elseif (isset($choices[$value]))
             $selection[] = $choices[$value];
         elseif ($this->get('default'))
@@ -786,8 +785,8 @@ class ChoiceField extends FormField {
         return $selection ? implode(', ', array_filter($selection)) : '';
     }
 
-    function getChoices() {
-        if ($this->_choices === null) {
+    function getChoices($verbose=false) {
+        if ($this->_choices === null || $verbose) {
             // Allow choices to be set in this->ht (for configurationOptions)
             $this->_choices = $this->get('choices');
             if (!$this->_choices) {
@@ -801,6 +800,18 @@ class ChoiceField extends FormField {
                         $val = $key;
                     $this->_choices[trim($key)] = trim($val);
                 }
+                // Add old selections if nolonger available
+                // This is necessary so choices made previously can be
+                // retained
+                $values = ($a=$this->getAnswer()) ? $a->getValue() : array();
+                if ($values && is_array($values)) {
+                    foreach ($values as $k => $v) {
+                        if (!isset($this->_choices[$k])) {
+                            if ($verbose) $v .= ' (retired)';
+                            $this->_choices[$k] = $v;
+                        }
+                    }
+                }
             }
         }
         return $this->_choices;
@@ -1121,7 +1132,7 @@ class ChoicesWidget extends Widget {
         $config = $this->field->getConfiguration();
         // Determine the value for the default (the one listed if nothing is
         // selected)
-        $choices = $this->field->getChoices();
+        $choices = $this->field->getChoices(true);
         $prompt = $config['prompt'] ?: 'Select';
 
         $have_def = false;
@@ -1135,16 +1146,15 @@ class ChoicesWidget extends Widget {
             $have_def = isset($choices[$def_key]);
             $def_val = $have_def ? $choices[$def_key] : $prompt;
         }
-        $value = $this->value;
-        if ($value === null && $have_def)
-            $value = $def_key;
 
-        if ($value && is_array($value))
-            $values = $value;
-        elseif ($value !== null) // Assume multiselect (comma delimited values)
-            $values = explode(',', $value);
-        else
-            $values = array();
+        if (($value=$this->getValue()))
+            $values = $this->field->parse($value);
+        elseif ($this->value)
+             $values = $this->value;
+
+        if ($values === null)
+            $values = $have_def ? array($def_key => $choices[$def_key]) : array();
+
         ?>
         <span style="display:inline-block">
         <select name="<?php echo $this->name; ?>[]"
@@ -1156,11 +1166,11 @@ class ChoicesWidget extends Widget {
             <option value="<?php echo $def_key; ?>">&mdash; <?php
                 echo $def_val; ?> &mdash;</option>
             <?php }
-            foreach ($choices as $key=>$name) {
+            foreach ($choices as $key => $name) {
                 if (!$have_def && $key == $def_key)
                     continue; ?>
                 <option value="<?php echo $key; ?>" <?php
-                    if (in_array($key, $values)) echo 'selected="selected"';
+                    if (isset($values[$key])) echo 'selected="selected"';
                 ?>><?php echo $name; ?></option>
             <?php } ?>
         </select>
diff --git a/include/class.list.php b/include/class.list.php
index d6080304a..89564ad62 100644
--- a/include/class.list.php
+++ b/include/class.list.php
@@ -892,11 +892,10 @@ class TicketStatus  extends VerySimpleModel implements CustomListItem {
     function setConfiguration(&$errors=array()) {
         $properties = array();
         foreach ($this->getConfigurationForm()->getFields() as $f) {
-            $val = $f->to_database($f->getClean());
+            $val = $f->getClean();
             $name = mb_strtolower($f->get('name'));
             switch ($name) {
                 case 'flags':
-                    $val = $f->getClean();
                     if ($val && is_array($val)) {
                         $flags = 0;
                         foreach ($val as $v) {
@@ -911,13 +910,14 @@ class TicketStatus  extends VerySimpleModel implements CustomListItem {
                     }
                     break;
                 case 'state':
+                    $val = $f->to_database($val);
                     if ($val && in_array($val, static::$_states))
                         $this->set('state', $val);
                     else
                         $f->addError('Unknown or invalid state', $name);
                     break;
                 default: //Custom properties the user might add.
-                    $properties[$f->get('id')] = $val;
+                    $properties[$f->get('id')] = $f->to_php($val);
             }
             $errors = array_merge($errors, $f->errors());
         }
-- 
GitLab