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> ' : ''; + + 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 — %s', + $list->getName(), __('Import Items')), + 'action' => "#list/{$list_id}/import", + 'upload_url' => "lists.php?id={$list_id}&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=" ".__('Showing')." "; 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}&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>»</strong></a>"; + $href = "{$file}&p={$nextspan}"; + if ($hash) + $href .= '#'.$hash; + $html .= "\n<a href=\"{$href}\" ><strong>»</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">— - <?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> ' : ''; - 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> </td> - <td> </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> <?php echo __('Copy Paste'); ?></a></li> + <li><a href="#upload" + ><i class="icon-fixed-width icon-cloud-upload"></i> <?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'); ?> — <?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(); ?> — <?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> ' : ''; + $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