From 2f34b5d3b2fa52d36a42530a7029088c68feb5dd Mon Sep 17 00:00:00 2001
From: Jared Hancock <jared@osticket.com>
Date: Thu, 15 Jan 2015 13:43:07 -0600
Subject: [PATCH] lists: Update list management page

---
 include/ajax.forms.php                        | 209 +++++++++++++++++-
 include/class.dynamic_forms.php               |   5 +-
 include/class.list.php                        | 155 ++++++++++++-
 include/class.orm.php                         |   3 +
 include/class.pagenate.php                    |  36 ++-
 include/staff/dynamic-list.inc.php            | 164 ++++----------
 include/staff/footer.inc.php                  |   9 +-
 include/staff/templates/list-import.tmpl.php  |  85 +++++++
 .../templates/list-item-properties.tmpl.php   | 113 +++++-----
 .../staff/templates/list-item-row.tmpl.php    |  37 ++++
 include/staff/templates/list-items.tmpl.php   |  99 +++++++++
 include/staff/templates/simple-form.tmpl.php  |   4 +-
 scp/ajax.php                                  |  11 +-
 scp/js/scp.js                                 |   9 +-
 scp/lists.php                                 |  75 +++----
 15 files changed, 753 insertions(+), 261 deletions(-)
 create mode 100644 include/staff/templates/list-import.tmpl.php
 create mode 100644 include/staff/templates/list-item-row.tmpl.php
 create mode 100644 include/staff/templates/list-items.tmpl.php

diff --git a/include/ajax.forms.php b/include/ajax.forms.php
index 169290c94..f710e64bb 100644
--- a/include/ajax.forms.php
+++ b/include/ajax.forms.php
@@ -96,29 +96,228 @@ class DynamicFormsAjaxAPI extends AjaxController {
         $ent->delete();
     }
 
-    function getListItemProperties($list_id, $item_id) {
+
+    function _getListItemEditForm($source=null, $item=false) {
+        return new Form(array(
+            'value' => new TextboxField(array(
+                'required' => true,
+                'label' => __('Value'),
+                'configuration' => array(
+                    'translatable' => $item ? $item->getTranslateTag('value') : false,
+                    'size' => 60,
+                    'length' => 0,
+                ),
+            )),
+            'extra' => new TextboxField(array(
+                'label' => __('Abbreviation'),
+                'configuration' => array(
+                    'size' => 60,
+                    'length' => 0,
+                ),
+            )),
+        ), $source);
+    }
+
+    function getListItem($list_id, $item_id) {
 
         $list = DynamicList::lookup($list_id);
         if (!$list || !($item = $list->getItem( (int) $item_id)))
             Http::response(404, 'No such list item');
 
+        $action = "#list/{$list->getId()}/item/{$item->getId()}/update";
+        $item_form = $this->_getListItemEditForm($item->ht, $item);
+
         include(STAFFINC_DIR . 'templates/list-item-properties.tmpl.php');
     }
 
-    function saveListItemProperties($list_id, $item_id) {
+    function getListItems($list_id) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login required');
+
+        if (!($list = DynamicList::lookup($list_id)))
+            Http::response(404, 'No such list');
+
+        $pjax_container = '#items';
+        include(STAFFINC_DIR . 'templates/list-items.tmpl.php');
+    }
+
+    function saveListItem($list_id, $item_id) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login required');
 
         $list = DynamicList::lookup($list_id);
         if (!$list || !($item = $list->getItem( (int) $item_id)))
             Http::response(404, 'No such list item');
 
-        if (!$item->setConfiguration()) {
+        $item_form = $this->_getListItemEditForm($_POST, $item);
+
+        if ($valid = $item_form->isValid()) {
+            // Update basic information
+            $basic = $item_form->getClean();
+            $item->extra = $basic['extra'];
+            $item->value = $basic['value'];
+
+            if ($_item = DynamicListItem::lookup(array('value'=>$item->value)))
+                if ($_item && $_item->id != $item->id)
+                    $item_form->getField('value')->addError(
+                        __('Value already in use'));
+        }
+
+        // Context
+        $action = "#list/{$list->getId()}/item/{$item->getId()}/update";
+        $icon = ($list->get('sort_mode') == 'SortCol')
+            ? '<i class="icon-sort"></i>&nbsp;' : '';
+
+        if (!$valid || !$item->setConfiguration()) {
             include STAFFINC_DIR . 'templates/list-item-properties.tmpl.php';
             return;
         }
-        else
+        else {
             $item->save();
+        }
+
+        // Send the whole row back
+        $prop_fields = array();
+        foreach ($list->getConfigurationForm()->getFields() as $f) {
+            if (in_array($f->get('type'), array('text', 'datetime', 'phone')))
+                $prop_fields[] = $f;
+            if (strpos($f->get('type'), 'list-') === 0)
+                $prop_fields[] = $f;
+
+            // 4 property columns max
+            if (count($prop_fields) == 4)
+                break;
+        }
+        ob_start();
+        $item->_config = null;
+        include STAFFINC_DIR . 'templates/list-item-row.tmpl.php';
+        $html = ob_get_clean();
+        Http::response(201, $this->encode(array(
+            'id' => $item->getId(),
+            'row' => $html,
+            'success' => true,
+        )));
+    }
+
+    function addListItem($list_id) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login required');
+        elseif (!($list = DynamicList::lookup($list_id)))
+            Http::response(404, 'No such list');
+
+        $action = "#list/{$list->getId()}/item/add";
+        $item_form = $this->_getListItemEditForm($_POST ?: null);
+
+        if ($_POST && ($valid = $item_form->isValid())) {
+            $data = $item_form->getClean();
+            if ($_item = DynamicListItem::lookup(array('value'=>$data['value'])))
+                if ($_item && $_item->id != $item->id)
+                    $item_form->getField('value')->addError(
+                        __('Value already in use'));
+            $data['list_id'] = $list_id;
+            $item = DynamicListItem::create($data);
+            if ($item->save() && $item->setConfiguration())
+                Http::response(201, $this->encode(array('success' => true)));
+        }
 
-        Http::response(201, 'Successfully updated record');
+        include(STAFFINC_DIR . 'templates/list-item-properties.tmpl.php');
+    }
+
+    function importListItems($list_id) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login required');
+        elseif (!($list = DynamicList::lookup($list_id)))
+            Http::response(404, 'No such list');
+
+        $info = array(
+            'title' => sprintf('%s &mdash; %s',
+                $list->getName(), __('Import Items')),
+            'action' => "#list/{$list_id}/import",
+            'upload_url' => "lists.php?id={$list_id}&amp;do=import-users",
+        );
+
+        if ($_POST) {
+            $status = $list->importFromPost($_POST['pasted']);
+            if (is_string($status))
+                $info['error'] = $status;
+            else
+                Http::response(201, $this->encode(array('success' => true, 'count' => $status)));
+        }
+
+        include(STAFFINC_DIR . 'templates/list-import.tmpl.php');
+    }
+
+    function disableItems($list_id) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login required');
+        elseif (!($list = DynamicList::lookup($list_id)))
+            Http::response(404, 'No such list');
+        elseif (!$_POST['ids'])
+            Http::response(422, 'Send `ids` parameter');
+
+        foreach ($_POST['ids'] as $id) {
+            if ($item = $list->getItem( (int) $id)) {
+                $item->disable();
+                $item->save();
+            }
+            else {
+                Http::response(404, 'No such list item');
+            }
+        }
+        Http::response(200, $this->encode(array('success' => true)));
+    }
+
+    function undisableItems($list_id) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login required');
+        elseif (!($list = DynamicList::lookup($list_id)))
+            Http::response(404, 'No such list');
+        elseif (!$_POST['ids'])
+            Http::response(422, 'Send `ids` parameter');
+
+        foreach ($_POST['ids'] as $id) {
+            if ($item = $list->getItem( (int) $id)) {
+                $item->enable();
+                $item->save();
+            }
+            else {
+                Http::response(404, 'No such list item');
+            }
+        }
+        Http::response(200, $this->encode(array('success' => true)));
+    }
+
+    function deleteItems($list_id) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login required');
+        elseif (!($list = DynamicList::lookup($list_id)))
+            Http::response(404, 'No such list');
+        elseif (!$_POST['ids'])
+            Http::response(422, 'Send `ids` parameter');
+
+        foreach ($_POST['ids'] as $id) {
+            if ($item = $list->getItem( (int) $id)) {
+                $item->delete();
+            }
+            else {
+                Http::response(404, 'No such list item');
+            }
+        }
+        #Http::response(200, $this->encode(array('success' => true)));
     }
 
     function upload($id) {
diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php
index e620669c9..2021ff656 100644
--- a/include/class.dynamic_forms.php
+++ b/include/class.dynamic_forms.php
@@ -582,7 +582,7 @@ class DynamicFormField extends VerySimpleModel {
     }
 
     function  isChangeable() {
-        return $this->hasFlag(self::FLAG_MASK_CHANGE);
+        return !$this->hasFlag(self::FLAG_MASK_CHANGE);
     }
 
     function  isEditable() {
@@ -1255,7 +1255,8 @@ class SelectionField extends FormField {
             }
         }
 
-        return $selection;
+        // Don't return an empty array
+        return $selection ?: null;
     }
 
     function to_database($value) {
diff --git a/include/class.list.php b/include/class.list.php
index df431b9ef..f8b457fb8 100644
--- a/include/class.list.php
+++ b/include/class.list.php
@@ -188,14 +188,14 @@ class DynamicList extends VerySimpleModel implements CustomList {
     }
 
     function getName() {
-        return $this->get('name');
+        return $this->getLocal('name');
     }
 
     function getPluralName() {
-        if ($name = $this->get('name_plural'))
+        if ($name = $this->getLocal('name_plural'))
             return $name;
         else
-            return $this->get('name') . 's';
+            return $this->getName . 's';
     }
 
     function getItemCount() {
@@ -447,6 +447,151 @@ class DynamicList extends VerySimpleModel implements CustomList {
         return $selections;
     }
 
+    function importCsv($stream, $defaults=array()) {
+        //Read the header (if any)
+        $headers = array('value' => __('Value'), 'abbrev' => __('Abbreviation'));
+        $form = $this->getConfigurationForm();
+        $named_fields = $fields = array(
+            'value' => new TextboxField(array(
+                'label' => __('Value'),
+                'name' => 'value',
+                'configuration' => array(
+                    'length' => 0,
+                ),
+            )),
+            'abbrev' => new TextboxField(array(
+                'name' => 'abbrev',
+                'label' => __('Abbreviation'),
+                'configuration' => array(
+                    'length' => 0,
+                ),
+            )),
+        );
+        $all_fields = $form->getFields();
+        $has_header = false;
+        foreach ($all_fields as $f)
+            if ($f->get('name'))
+                $named_fields[] = $f;
+
+        if (!($data = fgetcsv($stream, 1000, ",")))
+            return __('Whoops. Perhaps you meant to send some CSV records');
+
+        foreach ($data as $D) {
+            if (strcasecmp($D, 'value') === 0)
+                $has_header = true;
+        }
+        if ($has_header) {
+            foreach ($data as $h) {
+                $found = false;
+                foreach ($all_fields as $f) {
+                    if (in_array(mb_strtolower($h), array(
+                            mb_strtolower($f->get('name')), mb_strtolower($f->get('label'))))) {
+                        $found = true;
+                        if (!$f->get('name'))
+                            return sprintf(__(
+                                '%s: Field must have `variable` set to be imported'), $h);
+                        $headers[$f->get('name')] = $f->get('label');
+                        break;
+                    }
+                }
+                if (!$found) {
+                    $has_header = false;
+                    if (count($data) == count($named_fields)) {
+                        // Number of fields in the user form matches the number
+                        // of fields in the data. Assume things line up
+                        $headers = array();
+                        foreach ($named_fields as $f)
+                            $headers[$f->get('name')] = $f->get('label');
+                        break;
+                    }
+                    else {
+                        return sprintf(__('%s: Unable to map header to a property'), $h);
+                    }
+                }
+            }
+        }
+
+        // 'name' and 'email' MUST be in the headers
+        if (!isset($headers['value']))
+            return __('CSV file must include `value` column');
+
+        if (!$has_header)
+            fseek($stream, 0);
+
+        $items = array();
+        $keys = array('value', 'abbrev');
+        foreach ($headers as $h => $label) {
+            if (!($f = $form->getField($h)))
+                continue;
+
+            $name = $keys[] = $f->get('name');
+            $fields[$name] = $f->getImpl();
+        }
+
+        // Add default fields (org_id, etc).
+        foreach ($defaults as $key => $val) {
+            // Don't apply defaults which are also being imported
+            if (isset($header[$key]))
+                unset($defaults[$key]);
+            $keys[] = $key;
+        }
+
+        while (($data = fgetcsv($stream, 1000, ",")) !== false) {
+            if (count($data) == 1 && $data[0] == null)
+                // Skip empty rows
+                continue;
+            elseif (count($data) != count($headers))
+                return sprintf(__('Bad data. Expected: %s'), implode(', ', $headers));
+            // Validate according to field configuration
+            $i = 0;
+            foreach ($headers as $h => $label) {
+                $f = $fields[$h];
+                $T = $f->parse($data[$i]);
+                if ($f->validateEntry($T) && $f->errors())
+                    return sprintf(__(
+                        /* 1 will be a field label, and 2 will be error messages */
+                        '%1$s: Invalid data: %2$s'),
+                        $label, implode(', ', $f->errors()));
+                // Convert to database format
+                $data[$i] = $f->to_database($T);
+                $i++;
+            }
+            // Add default fields
+            foreach ($defaults as $key => $val)
+                $data[] = $val;
+
+            $items[] = $data;
+        }
+
+        $errors = array();
+        foreach ($items as $u) {
+            $vars = array_combine($keys, $u);
+            $item = $this->addItem($vars);
+            if (!$item || !$item->setConfiguration($errors, $vars))
+                return sprintf(__('Unable to import item: %s'),
+                    print_r($vars, true));
+        }
+
+        return count($items);
+    }
+
+    function importFromPost($stuff, $extra=array()) {
+        if (is_array($stuff) && !$stuff['error']) {
+            // Properly detect Macintosh style line endings
+            ini_set('auto_detect_line_endings', true);
+            $stream = fopen($stuff['tmp_name'], 'r');
+        }
+        elseif ($stuff) {
+            $stream = fopen('php://temp', 'w+');
+            fwrite($stream, $stuff);
+            rewind($stream);
+        }
+        else {
+            return __('Unable to parse submitted items');
+        }
+
+        return self::importCsv($stream, $extra);
+    }
 }
 FormField::addFieldTypes(/* @trans */ 'Custom Lists', array('DynamicList', 'getSelections'));
 
@@ -551,9 +696,9 @@ class DynamicListItem extends VerySimpleModel implements CustomListItem {
         return $this->_config;
     }
 
-    function setConfiguration(&$errors=array()) {
+    function setConfiguration(&$errors=array(), $source=false) {
         $config = array();
-        foreach ($this->getConfigurationForm($_POST)->getFields() as $field) {
+        foreach ($this->getConfigurationForm($source ?: $_POST)->getFields() as $field) {
             $config[$field->get('id')] = $field->to_php($field->getClean());
             $errors = array_merge($errors, $field->errors());
         }
diff --git a/include/class.orm.php b/include/class.orm.php
index 17dd05035..110e0d682 100644
--- a/include/class.orm.php
+++ b/include/class.orm.php
@@ -1741,6 +1741,9 @@ class MySqlCompiler extends SqlCompiler {
         elseif ($what instanceof SqlFunction) {
             return $what->toSql($this);
         }
+        elseif ($what === null) {
+            return 'NULL';
+        }
         else {
             switch ($slot) {
             case self::SLOT_JOINS:
diff --git a/include/class.pagenate.php b/include/class.pagenate.php
index 2d6e8b850..a696ce9f5 100644
--- a/include/class.pagenate.php
+++ b/include/class.pagenate.php
@@ -18,6 +18,7 @@ class PageNate {
 
     var $start;
     var $limit;
+    var $slack = 0;
     var $total;
     var $page;
     var $pages;
@@ -54,13 +55,16 @@ class PageNate {
     }
 
     function getStart() {
-        return $this->start;
+        return max($this->start + 1 - $this->slack, 1);
     }
 
     function getLimit() {
         return $this->limit;
     }
 
+    function setSlack($count) {
+        $this->slack = $count;
+    }
 
     function getNumPages(){
         return $this->pages;
@@ -72,27 +76,28 @@ class PageNate {
 
     function showing() {
         $html = '';
-        $from= $this->start+1;
-        if ($this->start + $this->limit < $this->total) {
-            $to= $this->start + $this->limit;
+        $start = $this->getStart();
+        $end = min($start + $this->limit + $this->slack - 1, $this->total);
+        if ($end < $this->total) {
+            $to= $end;
         } else {
             $to= $this->total;
         }
         $html="&nbsp;".__('Showing')."&nbsp;&nbsp;";
         if ($this->total > 0) {
             $html .= sprintf(__('%1$d - %2$d of %3$d' /* Used in pagination output */),
-               $from, $to, $this->total);
+               $start, $end, $this->total);
         }else{
             $html .= " 0 ";
         }
         return $html;
     }
 
-    function getPageLinks() {
+    function getPageLinks($hash=false, $pjax=false) {
         $html                 = '';
         $file                =$this->url;
         $displayed_span     = 5;
-        $total_pages         = ceil( $this->total / $this->limit );
+        $total_pages         = ceil( ($this->total - $this->slack) / $this->limit );
         $this_page             = ceil( ($this->start+1) / $this->limit );
 
         $last=$this_page-1;
@@ -116,15 +121,24 @@ class PageNate {
 
         for ($i=$start_loop; $i <= $stop_loop; $i++) {
             $page = ($i - 1) * $this->limit;
+            $href = "{$file}&amp;p={$i}";
+            if ($hash)
+                $href .= '#'.$hash;
             if ($i == $this_page) {
                 $html .= "\n<b>[$i]</b>";
+            }
+            elseif ($pjax) {
+                $html .= " <a href=\"{$href}\" data-pjax-container=\"{$pjax}\"><b>$i</b></a>";
             } else {
-                $html .= "\n<a href=\"$file&p=$i\" ><b>$i</b></a>";
+                $html .= "\n<a href=\"{$href}\" ><b>$i</b></a>";
             }
         }
         if($stop_loop<$total_pages){
             $nextspan=($stop_loop+$displayed_span>$total_pages)?$total_pages-$displayed_span:$stop_loop+$displayed_span;
-            $html .= "\n<a href=\"$file&p=$nextspan\" ><strong>&raquo;</strong></a>";
+            $href = "{$file}&amp;p={$nextspan}";
+            if ($hash)
+                $href .= '#'.$hash;
+            $html .= "\n<a href=\"{$href}\" ><strong>&raquo;</strong></a>";
         }
 
 
@@ -133,7 +147,9 @@ class PageNate {
     }
 
     function paginate(QuerySet $qs) {
-        return $qs->limit($this->getLimit())->offset($this->getStart());
+        $start = $this->getStart() - 1;
+        $end = min($start + $this->getLimit() + $this->slack + ($start > 0 ? $this->slack : 0), $this->total);
+        return $qs->limit($end-$start)->offset($start);
     }
 
 }
diff --git a/include/staff/dynamic-list.inc.php b/include/staff/dynamic-list.inc.php
index 9fe062173..6a0900990 100644
--- a/include/staff/dynamic-list.inc.php
+++ b/include/staff/dynamic-list.inc.php
@@ -121,6 +121,7 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info)
             <th nowrap></th>
             <th nowrap><?php echo __('Label'); ?></th>
             <th nowrap><?php echo __('Type'); ?></th>
+            <th nowrap><?php echo __('Visibility'); ?></th>
             <th nowrap><?php echo __('Variable'); ?></th>
             <th nowrap><?php echo __('Delete'); ?></th>
         </tr>
@@ -160,6 +161,8 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info)
                     href="#form/field-config/<?php
                         echo $f->get('id'); ?>"><i
                         class="icon-cog"></i> <?php echo __('Config'); ?></a> <?php } ?></td>
+            <td>
+                <?php echo $f->getVisibilityDescription(); ?></td>
             <td>
                 <input type="text" size="20" name="name-<?php echo $id; ?>"
                     value="<?php echo Format::htmlchars($f->get('name'));
@@ -200,6 +203,7 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info)
                 </optgroup>
                 <?php } ?>
             </select></td>
+            <td></td>
             <td><input type="text" size="20" name="name-new-<?php echo $i; ?>"
                 value="<?php echo $info["name-new-$i"]; ?>"/>
                 <font class="error"><?php
@@ -212,125 +216,9 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info)
 </table>
 </div>
 <div id="items" class="hidden tab_content">
-    <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
-    <thead>
-    <?php if ($list) {
-        $page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1;
-        $count = $list->getNumItems();
-        $pageNav = new Pagenate($count, $page, PAGE_LIMIT);
-        $pageNav->setURL('list.php', array('id' => $list->getId()));
-        $showing=$pageNav->showing().' '.__('list items');
-        ?>
-    <?php }
-        else $showing = __('Add a few initial items to the list');
-    ?>
-        <tr>
-            <th colspan="5">
-                <em><?php echo $showing; ?></em>
-            </th>
-        </tr>
-        <tr>
-            <th></th>
-            <th><?php echo __('Value'); ?></th>
-            <?php
-            if (!$list || $list->hasAbbrev()) { ?>
-            <th><?php echo __(/* Short for 'abbreviation' */ 'Abbrev'); ?> <em style="display:inline">&mdash;
-                <?php echo __('abbreviations and such'); ?></em></th>
-            <?php
-            } ?>
-            <th><?php echo __('Disabled'); ?></th>
-            <th><?php echo __('Delete'); ?></th>
-        </tr>
-    </thead>
-
-    <tbody <?php if ($info['sort_mode'] == 'SortCol') { ?>
-            class="sortable-rows" data-sort="sort-"<?php } ?>>
-        <?php
-        if ($list) {
-            $icon = ($info['sort_mode'] == 'SortCol')
-                ? '<i class="icon-sort"></i>&nbsp;' : '';
-        foreach ($list->getAllItems() as $i) {
-            $id = $i->getId(); ?>
-        <tr class="<?php if (!$i->isEnabled()) echo 'disabled'; ?>">
-            <td><?php echo $icon; ?>
-                <input type="hidden" name="sort-<?php echo $id; ?>"
-                value="<?php echo $i->getSortOrder(); ?>"/></td>
-            <td><input type="text" size="40" name="value-<?php echo $id; ?>"
-                data-translate-tag="<?php echo $i->getTranslateTag('value'); ?>"
-                value="<?php echo $i->getValue(); ?>"/>
-                <?php if ($list->hasProperties()) { ?>
-                   <a class="action-button field-config"
-                       style="overflow:inherit"
-                       href="#list/<?php
-                        echo $list->getId(); ?>/item/<?php
-                        echo $id ?>/properties"
-                       id="item-<?php echo $id; ?>"
-                    ><?php
-                        echo sprintf('<i class="icon-edit" %s></i> ',
-                                $i->getConfiguration()
-                                ? '': 'style="color:red; font-weight:bold;"');
-                        echo __('Properties');
-                   ?></a>
-                <?php
-                }
-
-                if ($errors["value-$id"])
-                    echo sprintf('<br><span class="error">%s</span>',
-                            $errors["value-$id"]);
-                ?>
-            </td>
-            <?php
-            if ($list->hasAbbrev()) { ?>
-            <td><input type="text" size="30" name="abbrev-<?php echo $id; ?>"
-                value="<?php echo $i->getAbbrev(); ?>"/></td>
-            <?php
-            } ?>
-            <td>
-                <?php
-                if (!$i->isDisableable())
-                     echo '<i class="icon-ban-circle"></i>';
-                else
-                    echo sprintf('<input type="checkbox" name="disable-%s"
-                            %s %s />',
-                            $id,
-                            !$i->isEnabled() ? ' checked="checked" ' : '',
-                            (!$i->isEnabled() && !$i->isEnableable()) ? ' disabled="disabled" ' : ''
-                            );
-                ?>
-            </td>
-            <td>
-                <?php
-                if (!$i->isDeletable())
-                    echo '<i class="icon-ban-circle"></i>';
-                else
-                    echo sprintf('<input type="checkbox" name="delete-item-%s">', $id);
-
-                ?>
-            </td>
-        </tr>
-    <?php }
-    }
-
-    if (!$list || $list->allowAdd()) {
-       for ($i=0; $i<$newcount; $i++) { ?>
-        <tr>
-            <td><?php echo $icon; ?> <em>+</em>
-                <input type="hidden" name="sort-new-<?php echo $i; ?>"/></td>
-            <td><input type="text" size="40" name="value-new-<?php echo $i; ?>"/></td>
-            <?php
-            if (!$list || $list->hasAbbrev()) { ?>
-            <td><input type="text" size="30" name="abbrev-new-<?php echo $i; ?>"/></td>
-            <?php
-            } ?>
-            <td>&nbsp;</td>
-            <td>&nbsp;</td>
-        </tr>
-    <?php
-       }
-    }?>
-    </tbody>
-    </table>
-</div>
+<?php
+    $pjax_container = '#items';
+    include STAFFINC_DIR . 'templates/list-items.tmpl.php'; ?>
 </div>
 <p class="centered">
     <input type="submit" name="submit" value="<?php echo $submit_text; ?>">
@@ -342,14 +230,46 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info)
 
 <script type="text/javascript">
 $(function() {
-    $('a.field-config').click( function(e) {
+    $(document).on('click', 'a.field-config', function(e) {
         e.preventDefault();
         var $id = $(this).attr('id');
         var url = 'ajax.php/'+$(this).attr('href').substr(1);
-        $.dialog(url, [201], function (xhr) {
-            $('a#'+$id+' i').removeAttr('style');
+        $.dialog(url, [201], function (xhr, resp) {
+          var json = $.parseJSON(resp);
+          if (json && json.success) {
+            if (json.id && json.row) {
+              $('#list-item-' + json.id).replaceWith(json.row);
+            }
+            else {
+              $.pjax.reload('#pjax-container');
+            }
+          }
         });
         return false;
     });
+    $(document).on('click', 'a.items-action', function(e) {
+        e.preventDefault();
+        var ids = [];
+        $('form#save :checkbox.mass:checked').each(function() {
+            ids.push($(this).val());
+        });
+        if (ids.length && confirm(__('You sure?'))) {
+            $.ajax({
+              url: 'ajax.php/' + $(this).attr('href').substr(1),
+              type: 'POST',
+              data: {count:ids.length, ids:ids},
+              dataType: 'json',
+              success: function(json) {
+                if (json.success) {
+                  if (window.location.search.indexOf('a=items') != -1)
+                    $.pjax.reload('#items');
+                  else
+                    $.pjax.reload('#pjax-container');
+                }
+              }
+            });
+        }
+        return false;
+    });
 });
 </script>
diff --git a/include/staff/footer.inc.php b/include/staff/footer.inc.php
index 5abc09973..9f2ee556c 100644
--- a/include/staff/footer.inc.php
+++ b/include/staff/footer.inc.php
@@ -43,10 +43,11 @@ if(is_object($thisstaff) && $thisstaff->isStaff()) { ?>
 <script type="text/javascript">
 if ($.support.pjax) {
   $(document).on('click', 'a', function(event) {
-    if (!$(this).hasClass('no-pjax')
-        && !$(this).closest('.no-pjax').length
-        && $(this).attr('href')[0] != '#')
-      $.pjax.click(event, {container: $('#pjax-container'), timeout: 2000});
+    var $this = $(this);
+    if (!$this.hasClass('no-pjax')
+        && !$this.closest('.no-pjax').length
+        && $this.attr('href')[0] != '#')
+      $.pjax.click(event, {container: $this.data('pjaxContainer') || $('#pjax-container'), timeout: 2000});
   })
 }
 </script>
diff --git a/include/staff/templates/list-import.tmpl.php b/include/staff/templates/list-import.tmpl.php
new file mode 100644
index 000000000..b3c38a99d
--- /dev/null
+++ b/include/staff/templates/list-import.tmpl.php
@@ -0,0 +1,85 @@
+<h3><?php echo $info['title']; ?></h3>
+<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
+<hr/>
+<?php
+if ($info['error']) {
+    echo sprintf('<p id="msg_error">%s</p>', $info['error']);
+} elseif ($info['warn']) {
+    echo sprintf('<p id="msg_warning">%s</p>', $info['warn']);
+} elseif ($info['msg']) {
+    echo sprintf('<p id="msg_notice">%s</p>', $info['msg']);
+} ?>
+<ul class="tabs" id="user-import-tabs">
+    <li class="active"><a href="#copy-paste"
+        ><i class="icon-edit"></i>&nbsp;<?php echo __('Copy Paste'); ?></a></li>
+    <li><a href="#upload"
+        ><i class="icon-fixed-width icon-cloud-upload"></i>&nbsp;<?php echo __('Upload'); ?></a></li>
+</ul>
+
+<form action="<?php echo $info['action']; ?>" method="post" enctype="multipart/form-data"
+    onsubmit="javascript:
+    if ($(this).find('[name=import]').val()) {
+        $(this).attr('action', '<?php echo $info['upload_url']; ?>');
+        $(document).unbind('submit.dialog');
+    }">
+<?php echo csrf_token();
+if ($org_id) { ?>
+    <input type="hidden" name="id" value="<?php echo $org_id; ?>"/>
+<?php } ?>
+<div id="user-import-tabs_container">
+<div class="tab_content" id="copy-paste" style="margin:5px;">
+<h2 style="margin-bottom:10px"><?php echo __('Value and Abbreviation'); ?></h2>
+<p><?php echo __(
+'Enter one name and abbreviation per line.'); ?><br/><em><?php echo __(
+'To import items with properties, use the Upload tab.'); ?></em>
+</p>
+<textarea name="pasted" style="display:block;width:100%;height:8em;padding:5px"
+    placeholder="<?php echo __('e.g. My Location, MY'); ?>">
+<?php echo $info['pasted']; ?>
+</textarea>
+</div>
+
+<div class="hidden tab_content" id="upload" style="margin:5px;">
+<h2 style="margin-bottom:10px"><?php echo __('Import a CSV File'); ?></h2>
+<p>
+<em><?php echo __(
+'Use the columns shown in the table below. To add more properties, use the Properties tab.  Only properties with `variable` defined can be imported.'); ?>
+</p>
+<table class="list"><tr>
+<?php
+    $fields = array('Value', 'Abbreviation');
+    $data = array(
+        array('Value' => __('My Location'), 'Abbreviation' => 'MY')
+    );
+    foreach ($list->getConfigurationForm()->getFields() as $f)
+        if ($f->get('name'))
+            $fields[] = $f->get('label');
+    foreach ($fields as $f) { ?>
+        <th><?php echo mb_convert_case($f, MB_CASE_TITLE); ?></th>
+<?php } ?>
+</tr>
+<?php
+    foreach ($data as $d) {
+        foreach ($fields as $f) {
+            ?><td><?php
+            if (isset($d[$f])) echo $d[$f];
+            ?></td><?php
+        }
+    } ?>
+</tr></table>
+<br/>
+<input type="file" name="import"/>
+</div>
+
+    <hr>
+    <p class="full-width">
+        <span class="buttons pull-left">
+            <input type="reset" value="<?php echo __('Reset'); ?>">
+            <input type="button" name="cancel" class="close"  value="<?php
+            echo __('Cancel'); ?>">
+        </span>
+        <span class="buttons pull-right">
+            <input type="submit" value="<?php echo __('Import Items'); ?>">
+        </span>
+     </p>
+</form>
diff --git a/include/staff/templates/list-item-properties.tmpl.php b/include/staff/templates/list-item-properties.tmpl.php
index def77747a..706c9484c 100644
--- a/include/staff/templates/list-item-properties.tmpl.php
+++ b/include/staff/templates/list-item-properties.tmpl.php
@@ -1,63 +1,52 @@
-    <h3><?php echo __('Item Properties'); ?> &mdash; <?php echo $item->getValue() ?></h3>
-    <a class="close" href=""><i class="icon-remove-circle"></i></a>
-    <hr/>
-    <form method="post" action="#list/<?php
-            echo $list->getId(); ?>/item/<?php
-            echo $item->getId(); ?>/properties">
-        <?php
-        echo csrf_token();
-        $config = $item->getConfiguration();
-        $internal = $item->isInternal();
-        $form = $item->getConfigurationForm();
-        echo $form->getMedia();
-        foreach ($form->getFields() as $f) {
-            ?>
-            <div class="custom-field" id="field<?php
-                echo $f->getWidget()->id; ?>"
-                <?php
-                if (!$f->isVisible()) echo 'style="display:none;"'; ?>>
-            <div class="field-label">
-            <label for="<?php echo $f->getWidget()->name; ?>"
-                style="vertical-align:top;padding-top:0.2em">
-                <?php echo Format::htmlchars($f->getLocal('label')); ?>:</label>
-                <?php
-                if (!$internal && $f->isEditable() && $f->get('hint')) { ?>
-                    <br /><em style="color:gray;display:inline-block"><?php
-                        echo Format::htmlchars($f->get('hint')); ?></em>
-                <?php
-                } ?>
-            </div><div>
-            <?php
-            if ($internal && !$f->isEditable())
-                $f->render(array('mode'=>'view'));
-            else {
-                $f->render();
-                if ($f->get('required')) { ?>
-                    <font class="error">*</font>
-                <?php
-                }
-            }
-            ?>
-            </div>
-            <?php
-            foreach ($f->errors() as $e) { ?>
-                <div class="error"><?php echo $e; ?></div>
-            <?php } ?>
-            </div>
-            <?php
-        }
-        ?>
-        </table>
-        <hr>
-        <p class="full-width">
-            <span class="buttons pull-left">
-                <input type="reset" value="<?php echo __('Reset'); ?>">
-                <input type="button" value="<?php echo __('Cancel'); ?>" class="close">
-            </span>
-            <span class="buttons pull-right">
-                <input type="submit" value="<?php echo __('Save'); ?>">
-            </span>
-         </p>
-    </form>
-    <div class="clear"></div>
+<h3><?php echo $list->getName(); ?> &mdash; <?php
+    echo $item ? $item->getValue() : __('Add New List Item'); ?></h3>
+<a class="close" href=""><i class="icon-remove-circle"></i></a>
+<hr/>
 
+<ul class="tabs" id="item_tabs">
+    <li class="active">
+        <a href="#value"><i class="icon-reorder"></i>
+        <?php echo __('Value'); ?></a>
+    </li>
+    <li><a href="#properties"><i class="icon-asterisk"></i>
+        <?php echo __('Item Properties'); ?></a>
+    </li>
+</ul>
+
+<form method="post" id="item_tabs_container" action="<?php echo $action; ?>">
+    <?php
+    echo csrf_token();
+    $internal = $item ? $item->isInternal() : false;
+?>
+
+<div class="tab_content" id="value">
+<?php
+    $form = $item_form;
+    include 'dynamic-form-simple.tmpl.php';
+?>
+</div>
+
+<div class="tab_content hidden" id="properties">
+<?php
+    $form = $item ? $item->getConfigurationForm($_POST ?: null)
+        : $list->getConfigurationForm();
+    include 'dynamic-form-simple.tmpl.php';
+?>
+</div>
+
+<hr>
+<p class="full-width">
+    <span class="buttons pull-left">
+        <input type="reset" value="<?php echo __('Reset'); ?>">
+        <input type="button" value="<?php echo __('Cancel'); ?>" class="close">
+    </span>
+    <span class="buttons pull-right">
+        <input type="submit" value="<?php echo __('Save'); ?>">
+    </span>
+ </p>
+</form>
+
+<script type="text/javascript">
+   // Make translatable fields translatable
+   $('input[data-translate-tag], textarea[data-translate-tag]').translatable();
+</script>
diff --git a/include/staff/templates/list-item-row.tmpl.php b/include/staff/templates/list-item-row.tmpl.php
new file mode 100644
index 000000000..7df1517ef
--- /dev/null
+++ b/include/staff/templates/list-item-row.tmpl.php
@@ -0,0 +1,37 @@
+<?php
+    $id = $item->getId(); ?>
+    <tr id="list-item-<?php echo $id; ?>" class="<?php if (!$item->isEnabled()) echo 'disabled'; ?>">
+        <td nowrap><?php echo $icon; ?>
+            <input type="hidden" name="sort-<?php echo $id; ?>"
+            value="<?php echo $item->getSortOrder(); ?>"/>
+            <input type="checkbox" value="<?php echo $id; ?>" class="mass nowarn"/>
+        </td>
+        <td>
+            <a class="field-config"
+               style="overflow:inherit"
+               href="#list/<?php
+                echo $list->getId(); ?>/item/<?php
+                echo $id ?>/update"
+               id="item-<?php echo $id; ?>"
+            ><?php
+                echo sprintf('<i class="icon-edit" %s></i> ',
+                        $item->getConfiguration()
+                        ? '': 'style="color:red; font-weight:bold;"');
+            ?>
+            <?php echo Format::htmlchars($item->getValue()); ?>
+            <?php
+            if ($list->hasAbbrev() && ($A = $item->getAbbrev())) { ?>
+                ( <?php echo Format::htmlchars($A); ?> )
+            <?php
+            } ?>
+<?php           if ($errors["value-$id"])
+                echo sprintf('<div class="error">%s</div>',
+                        $errors["value-$id"]);
+            ?>
+            </a>
+        </td>
+<?php $props = $item->getConfiguration();
+    foreach ($prop_fields as $F) { ?>
+        <td><?php echo $F->display($props[$F->get('id')]); ?></td>
+<?php } ?>
+    </tr>
diff --git a/include/staff/templates/list-items.tmpl.php b/include/staff/templates/list-items.tmpl.php
new file mode 100644
index 000000000..e9d685024
--- /dev/null
+++ b/include/staff/templates/list-items.tmpl.php
@@ -0,0 +1,99 @@
+    <?php if ($list) {
+        $page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1;
+        $count = $list->getNumItems();
+        $pageNav = new Pagenate($count, $page, PAGE_LIMIT);
+        if ($list->getSortMode() == 'SortCol')
+            $pageNav->setSlack(1);
+        $pageNav->setURL('lists.php?id='.$list->getId().'&a=items');
+        $showing=$pageNav->showing().' '.__('list items');
+        ?>
+    <?php }
+        else $showing = __('Add a few initial items to the list');
+    ?>
+    <div style="margin: 5px 0">
+    <div class="pull-left"><em><?php echo $showing; ?></em></div>
+    <div class="pull-right">
+        <?php if (!$list || $list->allowAdd()) { ?>
+        <a class="action-button field-config"
+            href="#list/<?php
+            echo $list->getId(); ?>/item/add">
+            <i class="icon-plus-sign"></i>
+            <?php echo __('Add New Item'); ?>
+        </a>
+        <a class="action-button field-config"
+            href="#list/<?php
+            echo $list->getId(); ?>/import">
+            <i class="icon-upload"></i>
+            <?php echo __('Import Items'); ?>
+        </a>
+        <?php } ?>
+        <span class="action-button pull-right" data-dropdown="#action-dropdown-more">
+            <i class="icon-caret-down pull-right"></i>
+            <span ><i class="icon-cog"></i> <?php echo __('More');?></span>
+        </span>
+        <div id="action-dropdown-more" class="action-dropdown anchor-right">
+            <ul>
+                <li><a class="items-action" href="#list/<?php echo $list->getId(); ?>/delete">
+                    <i class="icon-trash icon-fixed-width"></i>
+                    <?php echo __('Delete'); ?></a></li>
+                <li><a class="items-action" href="#list/<?php echo $list->getId(); ?>/disable">
+                    <i class="icon-ban-circle icon-fixed-width"></i>
+                    <?php echo __('Disable'); ?></a></li>
+                <li><a class="items-action" href="#list/<?php echo $list->getId(); ?>/enable">
+                    <i class="icon-ok-sign icon-fixed-width"></i>
+                    <?php echo __('Enable'); ?></a></li>
+            </ul>
+        </div>
+    </div>
+
+    <div class="clear"></div>
+    </div>
+
+
+<?php
+$prop_fields = array();
+if ($list) {
+    foreach ($list->getConfigurationForm()->getFields() as $f) {
+        if (in_array($f->get('type'), array('text', 'datetime', 'phone')))
+            $prop_fields[] = $f;
+        if (strpos($f->get('type'), 'list-') === 0)
+            $prop_fields[] = $f;
+
+        // 4 property columns max
+        if (count($prop_fields) == 4)
+            break;
+    }
+}
+?>
+
+    <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
+    <thead>
+        <tr>
+            <th width="8" nowrap></th>
+            <th><?php echo __('Value'); ?></th>
+<?php foreach ($prop_fields as $F) { ?>
+            <th><?php echo $F->getLocal('label'); ?></th>
+<?php } ?>
+        </tr>
+    </thead>
+
+    <tbody <?php if ($list->get('sort_mode') == 'SortCol') { ?>
+            class="sortable-rows" data-sort="sort-"<?php } ?>>
+        <?php
+        if ($list) {
+            $icon = ($list->get('sort_mode') == 'SortCol')
+                ? '<i class="icon-sort"></i>&nbsp;' : '';
+            $items = $pageNav->paginate($list->getAllItems());
+            // Emit a marker for the first sort offset ?>
+            <input type="hidden" id="sort-offset" value="<?php echo
+                max($items[0]->sort, $pageNav->getStart()); ?>"/>
+<?php
+            foreach ($items as $item) {
+                include STAFFINC_DIR . 'templates/list-item-row.tmpl.php';
+            }
+        } ?>
+    </tbody>
+    </table>
+    <div><?php echo __('Page').':'.$pageNav->getPageLinks('items', $pjax_container); ?></div>
+</div>
+
diff --git a/include/staff/templates/simple-form.tmpl.php b/include/staff/templates/simple-form.tmpl.php
index 2191e474d..705592fcf 100644
--- a/include/staff/templates/simple-form.tmpl.php
+++ b/include/staff/templates/simple-form.tmpl.php
@@ -8,7 +8,7 @@
         <div class="form-field"><?php
         if (!$field->isBlockLevel()) { ?>
             <div class="<?php if ($field->isRequired()) echo 'required';
-                ?>" style="display:inline-block;width:260px;">
+                ?>" style="display:inline-block;width:27%;">
                 <?php echo Format::htmlchars($field->getLocal('label')); ?>:
             <?php if ($field->isRequired()) { ?>
                 <span class="error">*</span>
@@ -20,7 +20,7 @@
                 ?></div>
 <?php       } ?>
             </div>
-            <div style="display:inline-block;max-width:700px"><?php
+            <div style="display:inline-block;max-width:73%"><?php
         }
         $field->render($options);
         foreach ($field->errors() as $e) { ?>
diff --git a/scp/ajax.php b/scp/ajax.php
index 140085b39..ed9e37345 100644
--- a/scp/ajax.php
+++ b/scp/ajax.php
@@ -64,8 +64,15 @@ $dispatcher = patterns('',
         url_get('^action/(?P<type>\w+)/config$', 'getFilterActionForm')
     )),
     url('^/list/', patterns('ajax.forms.php:DynamicFormsAjaxAPI',
-        url_get('^(?P<list>\w+)/item/(?P<id>\d+)/properties$', 'getListItemProperties'),
-        url_post('^(?P<list>\w+)/item/(?P<id>\d+)/properties$', 'saveListItemProperties')
+        url_get('^(?P<list>\w+)/items$', 'getListItems'),
+        url_get('^(?P<list>\w+)/item/(?P<id>\d+)/update$', 'getListItem'),
+        url_post('^(?P<list>\w+)/item/(?P<id>\d+)/update$', 'saveListItem'),
+        url('^(?P<list>\w+)/item/add$', 'addListItem'),
+        url('^(?P<list>\w+)/import$', 'importListItems'),
+        url('^(?P<list>\w+)/manage$', 'massManageListItems'),
+        url_post('^(?P<list>\w+)/delete$', 'deleteItems'),
+        url_post('^(?P<list>\w+)/disable$', 'disableItems'),
+        url_post('^(?P<list>\w+)/enable$', 'undisableItems')
     )),
     url('^/report/overview/', patterns('ajax.reports.php:OverviewReportAjaxAPI',
         # Send
diff --git a/scp/js/scp.js b/scp/js/scp.js
index 1dfeefd1d..a3a531d29 100644
--- a/scp/js/scp.js
+++ b/scp/js/scp.js
@@ -153,7 +153,7 @@ var scp_prep = function() {
     };
 
     $("form#save :input").change(function() {
-        warnOnLeave($(this));
+        if (!$(this).is('.nowarn')) warnOnLeave($(this));
     });
 
     $("form#save :input[type=reset]").click(function() {
@@ -431,10 +431,11 @@ var scp_prep = function() {
        'helper': fixHelper,
        'cursor': 'move',
        'stop': function(e, ui) {
-           var attr = ui.item.parent('tbody').data('sort');
+           var attr = ui.item.parent('tbody').data('sort'),
+               offset = parseInt($('#sort-offset').val(), 10) || 0;
            warnOnLeave(ui.item);
            $('input[name^='+attr+']', ui.item.parent('tbody')).each(function(i, el) {
-               $(el).val(i+1);
+               $(el).val(i + 1 + offset);
            });
        }
    });
@@ -587,7 +588,7 @@ $.dialog = function (url, codes, cb, options) {
                         $.toggleOverlay(false);
                         $popup.hide();
                         $('div.body', $popup).empty();
-                        if(cb) cb(xhr);
+                        if(cb) cb(xhr, resp);
                     } else {
                         try {
                             var json = $.parseJSON(resp);
diff --git a/scp/lists.php b/scp/lists.php
index 5247a0643..0d99e9cfc 100644
--- a/scp/lists.php
+++ b/scp/lists.php
@@ -21,7 +21,6 @@ if ($criteria) {
 }
 
 $errors = array();
-$max_isort = 0;
 
 if($_POST) {
     switch(strtolower($_POST['do'])) {
@@ -30,36 +29,15 @@ if($_POST) {
                 $errors['err']=sprintf(__('%s: Unknown or invalid ID.'),
                     __('custom list'));
             elseif ($list->update($_POST, $errors)) {
-                // Update items
-                $items = array();
-                foreach ($list->getAllItems() as $item) {
-                    $id = $item->getId();
-                    if ($_POST["delete-item-$id"] == 'on' && $item->isDeletable()) {
-                        $item->delete();
-                        continue;
-                    }
-
-                    $ht = array(
-                            'value' => $_POST["value-$id"],
-                            'abbrev' => $_POST["abbrev-$id"],
-                            'sort' => $_POST["sort-$id"],
-                            );
-                    $value = mb_strtolower($ht['value']);
-                    if (!$value)
-                        $errors["value-$id"] = __('Value required');
-                    elseif (in_array($value, $items))
-                        $errors["value-$id"] = __('Value already in-use');
-                    elseif ($item->update($ht, $errors)) {
-                        if ($_POST["disable-$id"] == 'on')
-                            $item->disable();
-                        elseif(!$item->isEnabled() && $item->isEnableable())
-                            $item->enable();
-
-                        $item->save();
-                        $items[] = $value;
+                // Update item sorting
+                if ($list->getSortMode() == 'SortCol') {
+                    foreach ($list->getAllItems() as $item) {
+                        $id = $item->getId();
+                        if (isset($_POST["sort-{$id}"])) {
+                            $item->sort = $_POST["sort-$id"];
+                            $item->save();
+                        }
                     }
-
-                    $max_isort = max($max_isort, $_POST["sort-$id"]);
                 }
 
                 // Update properties
@@ -154,19 +132,20 @@ if($_POST) {
                 }
             }
             break;
-    }
-
-    if ($list && $list->allowAdd()) {
-        for ($i=0; isset($_POST["sort-new-$i"]); $i++) {
-            if (!$_POST["value-new-$i"])
-                continue;
-
-            $list->addItem(array(
-                        'value' => $_POST["value-new-$i"],
-                        'abbrev' =>$_POST["abbrev-new-$i"],
-                        'sort' => $_POST["sort-new-$i"] ?: ++$max_isort,
-                        ), $errors);
-        }
+        case 'import-items':
+            if (!$list) {
+                $errors['err']=sprintf(__('%s: Unknown or invalid ID.'),
+                    __('custom list'));
+            }
+            else {
+                $status = $list->importFromPost($_FILES['import'] ?: $_POST['pasted']);
+                if (is_numeric($status))
+                    $msg = sprintf(__('Successfully imported %1$d %2$s.'), $status,
+                        _N('list item', 'list items', $status));
+                else
+                    $errors['err'] = $status;
+            }
+            break;
     }
 
     if ($form) {
@@ -179,6 +158,9 @@ if($_POST) {
                 'label' => $_POST["prop-label-new-$i"],
                 'type' => $_POST["type-new-$i"],
                 'name' => $_POST["name-new-$i"],
+                'flags' => DynamicFormField::FLAG_ENABLED
+                    | DynamicFormField::FLAG_AGENT_VIEW
+                    | DynamicFormField::FLAG_AGENT_EDIT,
             ));
             $field->setForm($form);
             if ($field->isValid())
@@ -193,6 +175,13 @@ if($_POST) {
 }
 
 $page='dynamic-lists.inc.php';
+if($list && !strcasecmp(@$_REQUEST['a'],'items') && isset($_SERVER['HTTP_X_PJAX'])) {
+    $page='templates/list-items.tmpl.php';
+    $pjax_container = @$_SERVER['HTTP_X_PJAX_CONTAINER'];
+    require(STAFFINC_DIR.$page);
+    // Don't emit the header
+    return;
+}
 if($list || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add'))) {
     $page='dynamic-list.inc.php';
     $ost->addExtraHeader('<meta name="tip-namespace" content="manage.custom_list" />',
-- 
GitLab