diff --git a/bootstrap.php b/bootstrap.php index 6d7acf23f8152980feabb54d00ffdf3cb0d53b6a..8a68c184342105d0bb5d3583a8a842d389de3426 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -135,6 +135,8 @@ class Bootstrap { define('QUEUE_TABLE', $prefix.'queue'); define('COLUMN_TABLE', $prefix.'queue_column'); define('QUEUE_COLUMN_TABLE', $prefix.'queue_columns'); + define('QUEUE_SORT_TABLE', $prefix.'queue_sort'); + define('QUEUE_SORTING_TABLE', $prefix.'queue_sorts'); define('API_KEY_TABLE',$prefix.'api_key'); define('TIMEZONE_TABLE',$prefix.'timezone'); diff --git a/include/ajax.admin.php b/include/ajax.admin.php index 6869eaad000070c8bb1c2569f7d51a6daa77ff4a..1047b8395d101d93dac8990ce2022c4e178c6825 100644 --- a/include/ajax.admin.php +++ b/include/ajax.admin.php @@ -215,4 +215,29 @@ class AdminAjaxAPI extends AjaxController { include STAFFINC_DIR . 'templates/queue-column-add.tmpl.php'; } + + function addQueueSort($root='Ticket') { + global $ost, $thisstaff; + + if (!$thisstaff) + Http::response(403, 'Agent login required'); + if (!$thisstaff->isAdmin()) + Http::response(403, 'Access denied'); + + $sort = new QueueSort(); + if ($_POST) { + $data_form = $sort->getDataConfigForm($_POST); + if ($data_form->isValid()) { + $sort->update($data_form->getClean() + $_POST, $root); + if ($sort->save()) + Http::response(201, $this->encode(array( + 'id' => $sort->getId(), + 'name' => (string) $sort->getName(), + ), 'application/json')); + } + } + + include STAFFINC_DIR . 'templates/queue-sorting-add.tmpl.php'; + + } } diff --git a/include/ajax.search.php b/include/ajax.search.php index 4a735f304055ed761ce9c148564aef6edc6ff62f..3f78b93f1d2269db20852740362aee0931418bf4 100644 --- a/include/ajax.search.php +++ b/include/ajax.search.php @@ -239,6 +239,28 @@ class SearchAjaxAPI extends AjaxController { include STAFFINC_DIR . 'templates/queue-column-edit.tmpl.php'; } + function editSort($sort_id) { + global $thisstaff; + + if (!$thisstaff) { + Http::response(403, 'Agent login is required'); + } + elseif (!($sort = QueueSort::lookup($sort_id))) { + Http::response(404, 'No such queue sort'); + } + + if ($_POST) { + $data_form = $sort->getDataConfigForm($_POST); + if ($data_form->isValid()) { + $sort->update($data_form->getClean() + $_POST); + if ($sort->save()) + Http::response(201, 'Successfully updated'); + } + } + + include STAFFINC_DIR . 'templates/queue-sorting-edit.tmpl.php'; + } + function previewQueue($id=false) { global $thisstaff; diff --git a/include/class.i18n.php b/include/class.i18n.php index b9bac6202c110bb9652ded88381e8faba3979cf8..0a08736d76d84cc4e120cd73215a48a161a659f0 100644 --- a/include/class.i18n.php +++ b/include/class.i18n.php @@ -67,6 +67,7 @@ class Internationalization { 'file.yaml' => 'AttachmentFile::__create', 'sequence.yaml' => 'Sequence::__create', 'queue_column.yaml' => 'QueueColumn::__create', + 'queue_sort.yaml' => 'QueueSort::__create', 'queue.yaml' => 'CustomQueue::__create', ); diff --git a/include/class.orm.php b/include/class.orm.php index 8828103ad7e505dce76330bb9081b149db2836dd..7756fec739ccd369220fc576d3d8f2524b9ce3f1 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -502,6 +502,15 @@ class VerySimpleModel { function __onload() {} + function serialize() { + return $this->getPk(); + } + + function unserialize($data) { + $this->ht = $data; + $this->refetch(); + } + static function getMeta($key=false) { if (!static::$meta instanceof ModelMeta || get_called_class() != static::$meta->model @@ -671,9 +680,7 @@ class VerySimpleModel { if ($refetch) { // Preserve non database information such as list relationships // across the refetch - $this->ht = - static::objects()->filter($this->getPk())->values()->one() - + $this->ht; + $this->refetch(); } if ($wasnew) { // Attempt to update foreign, unsaved objects with the PK of @@ -702,6 +709,12 @@ class VerySimpleModel { return true; } + private function refetch() { + $this->ht = + static::objects()->filter($this->getPk())->values()->one() + + $this->ht; + } + private function getPk() { $pk = array(); foreach ($this::getMeta('pk') as $f) @@ -752,7 +765,7 @@ END_CLASS } trait AnnotatedModelTrait { - function get($what) { + function get($what, $default=false) { if (isset($this->__overlay__[$what])) return $this->__overlay__[$what]; return parent::get($what); @@ -784,7 +797,7 @@ trait AnnotatedModelTrait { * is, the annotated model will remain). */ trait WriteableAnnotatedModelTrait { - function get($what) { + function get($what, $default=false) { if ($this->__overlay__->__isset($what)) return $this->__overlay__->get($what); return parent::get($what); diff --git a/include/class.queue.php b/include/class.queue.php index be5ca24ef1b21210df9744306443c99dfa2edb1a..c57630e69f4c84d44d709b6040f41237a64098a8 100644 --- a/include/class.queue.php +++ b/include/class.queue.php @@ -30,6 +30,10 @@ class CustomQueue extends VerySimpleModel { 'reverse' => 'QueueColumnGlue.queue', 'broker' => 'QueueColumnListBroker', ), + 'sorts' => array( + 'reverse' => 'QueueSortGlue.queue', + 'broker' => 'QueueSortListBroker', + ), 'parent' => array( 'constraint' => array( 'parent_id' => 'CustomQueue.id', @@ -49,6 +53,7 @@ class CustomQueue extends VerySimpleModel { const FLAG_CONTAINER = 0x0004; // Container for other queues ('Open') const FLAG_INHERIT_CRITERIA = 0x0008; // Include criteria from parent const FLAG_INHERIT_COLUMNS = 0x0010; // Inherit column layout from parent + const FLAG_INHERIT_SORTING = 0x0020; // Inherit advanced sorting from parent var $criteria; @@ -524,6 +529,13 @@ class CustomQueue extends VerySimpleModel { $col->queue = $this; } + function getSortOptions() { + if ($this->inheritSorting() && $this->parent) { + return $this->parent->getSortOptions(); + } + return $this->sorts; + } + function getStatus() { return 'bogus'; } @@ -679,6 +691,10 @@ class CustomQueue extends VerySimpleModel { return $this->hasFlag(self::FLAG_INHERIT_COLUMNS); } + function inheritSorting() { + return $this->hasFlag(self::FLAG_INHERIT_SORTING); + } + function buildPath() { if (!$this->id) return; @@ -734,6 +750,8 @@ class CustomQueue extends VerySimpleModel { $this->parent_id > 0 && isset($vars['inherit'])); $this->setFlag(self::FLAG_INHERIT_COLUMNS, $this->parent_id > 0 && isset($vars['inherit-columns'])); + $this->setFlag(self::FLAG_INHERIT_SORTING, + $this->parent_id > 0 && isset($vars['inherit-sorting'])); // Update queue columns (but without save) if (!isset($vars['columns']) && $this->parent) { @@ -773,6 +791,32 @@ class CustomQueue extends VerySimpleModel { $this->columns->sort(function($c) { return $c->sort; }); } + // Update advanced sorting options for the queue + if (isset($vars['sorts']) && !$this->hasFlag(self::FLAG_INHERIT_SORTING)) { + $new = $vars['sorts']; + $order = array_keys($new); + foreach ($this->sorts as $sort) { + $key = $sort->sort_id; + if (!in_array($key, $vars['sorts'])) { + $this->sorts->remove($sort); + continue; + } + $sort->set('sort', array_search($key, $order)); + unset($new[$key]); + } + // Add new columns + foreach ($new as $id) { + $glue = new QueueSortGlue(array( + 'sort_id' => $id, + 'sort' => array_search($id, $order), + )); + $glue->queue = $this; + $this->sorts->add(QueueSort::lookup($id), $glue); + } + // Re-sort the in-memory columns array + $this->sorts->sort(function($c) { return $c->sort; }); + } + // TODO: Move this to SavedSearch::update() and adjust // AjaxSearch::_saveSearch() $form = $form ?: $this->getForm($vars); @@ -799,7 +843,8 @@ class CustomQueue extends VerySimpleModel { $this->path = $this->buildPath(); $this->save(); } - return $this->columns->saveAll(); + return $this->columns->saveAll() + && $this->sorts->saveAll(); } static function getOrmPath($name, $query=null) { @@ -851,6 +896,13 @@ class CustomQueue extends VerySimpleModel { $glue->queue_id = $q->getId(); $glue->save(); } + if (isset($vars['sorts'])) { + foreach ($vars['sorts'] as $info) { + $glue = new QueueSortGlue($info); + $glue->queue_id = $q->getId(); + $glue->save(); + } + } return $q; } } @@ -1099,7 +1151,7 @@ extends QueueColumnAnnotation { class DataSourceField extends ChoiceField { - function getChoices() { + function getChoices($verbose=false) { $config = $this->getConfiguration(); $root = $config['root']; $fields = array(); @@ -1310,7 +1362,7 @@ extends ChoiceField { return new $choices(array('name' => $prop)); } - function getChoices() { + function getChoices($verbose=false) { if (isset($this->property)) return static::$properties[$this->property]; @@ -1722,7 +1774,7 @@ extends InstrumentedList { $this->queryset->select_related('column'); } - function add($column, $glue=null) { + function add($column, $glue=null, $php7_is_annoying=true) { $glue = $glue ?: new QueueColumnGlue(); $glue->column = $column; $anno = AnnotatedModel::wrap($column, $glue); @@ -1731,6 +1783,175 @@ extends InstrumentedList { } } +class QueueSort +extends VerySimpleModel { + static $meta = array( + 'table' => QUEUE_SORT_TABLE, + 'pk' => array('id'), + 'ordering' => array('name'), + 'joins' => array( + 'queue' => array( + 'constraint' => array('queue_id' => 'CustomQueue.id'), + ), + ), + ); + + var $_columns; + + function getRoot($hint=false) { + switch ($hint ?: $this->root) { + case 'T': + default: + return 'Ticket'; + } + } + + function getName() { + return $this->name; + } + + function getId() { + return $this->id; + } + + function applySort(QuerySet $query, $reverse=false, $root=false) { + $fields = CustomQueue::getSearchableFields($this->getRoot($root)); + foreach ($this->getColumnPaths() as $path=>$descending) { + $descending = $reverse ? !$descending : $descending; + if (isset($fields[$path])) { + list(,$field) = $fields[$path]; + $query = $field->applyOrderBy($query, $descending, + CustomQueue::getOrmPath($path, $query)); + } + } + return $query; + } + + function getColumnPaths() { + if (!isset($this->_columns)) { + $columns = array(); + foreach (JsonDataParser::decode($this->columns) as $path) { + if ($descending = $path[0] == '-') + $path = substr($path, 1); + $columns[$path] = $descending; + } + $this->_columns = $columns; + } + return $this->_columns; + } + + function getColumns() { + $columns = array(); + $paths = $this->getColumnPaths(); + $everything = CustomQueue::getSearchableFields($this->getRoot()); + foreach ($paths as $p=>$descending) { + if (isset($everything[$p])) { + $columns[$p] = array($everything[$p], $descending); + } + } + return $columns; + } + + function getDataConfigForm($source=false) { + return new QueueSortDataConfigForm($source ?: $this->getDbFields(), + array('id' => $this->id)); + } + + static function forQueue(CustomQueue $queue) { + return static::objects()->filter([ + 'root' => $queue->getRoot(), + ]); + } + + function save($refetch=false) { + if ($this->dirty) + $this->updated = SqlFunction::NOW(); + return parent::save($refetch || $this->dirty); + } + + function update($vars, &$errors=array()) { + if (!isset($vars['name'])) + $errors['name'] = __('A title is required'); + + $this->name = $vars['name']; + if (isset($vars['root'])) + $this->root = $vars['root']; + elseif (!isset($this->root)) + $this->root = 'T'; + + $fields = CustomQueue::getSearchableFields($this->getRoot($vars['root'])); + $columns = array(); + if (@is_array($vars['columns'])) { + foreach ($vars['columns']as $path=>$info) { + $descending = (int) @$info['descending']; + // TODO: Check if column is valid, stash in $columns + if (!isset($fields[$path])) + continue; + $columns[] = ($descending ? '-' : '') . $path; + } + $this->columns = JsonDataEncoder::encode($columns); + } + + if (count($errors)) + return false; + + return $this->save(); + } + + static function __create($vars) { + $c = new static($vars); + $c->save(); + return $c; + } +} + +class QueueSortGlue +extends VerySimpleModel { + static $meta = array( + 'table' => QUEUE_SORTING_TABLE, + 'pk' => array('sort_id', 'queue_id'), + 'joins' => array( + 'ordering' => array( + 'constraint' => array('sort_id' => 'QueueSort.id'), + ), + 'queue' => array( + 'constraint' => array('queue_id' => 'CustomQueue.id'), + ), + ), + 'select_related' => array('ordering', 'queue'), + 'ordering' => array('sort'), + ); +} + +class QueueSortGlueMIM +extends ModelInstanceManager { + function getOrBuild($modelClass, $fields, $cache=true) { + $m = parent::getOrBuild($modelClass, $fields, $cache); + if ($m && $modelClass === 'QueueSortGlue') { + // Instead, yield the QueueColumn instance with the local fields + // in the association table as annotations + $m = AnnotatedModel::wrap($m->ordering, $m, 'QueueSort'); + } + return $m; + } +} + +class QueueSortListBroker +extends InstrumentedList { + function __construct($fkey, $queryset=false) { + parent::__construct($fkey, $queryset, 'QueueSortGlueMIM'); + $this->queryset->select_related('ordering'); + } + + function add($ordering, $glue=null, $php7_is_annoying=true) { + $glue = $glue ?: new QueueSortGlue(); + $glue->ordering = $ordering; + $anno = AnnotatedModel::wrap($ordering, $glue); + parent::add($anno, false); + return $anno; + } +} + abstract class QueueColumnFilter { static $registry; @@ -1907,3 +2128,24 @@ extends AbstractForm { ); } } + +class QueueSortDataConfigForm +extends AbstractForm { + function getInstructions() { + return __('Add, and remove the fields in this list using the options below. Sorting is priortized in ascending order.'); + } + + function buildFields() { + return array( + 'name' => new TextboxField(array( + 'required' => true, + 'layout' => new GridFluidCell(12), + 'translatable' => isset($this->options['id']) + ? _H('queuesort.name.'.$this->options['id']) : false, + 'configuration' => array( + 'placeholder' => __('Sort Criteria Title'), + ), + )), + ); + } +} diff --git a/include/class.search.php b/include/class.search.php index 7dc62a5144c0c917eb392ba02dc109aaa49f9c66..1cb8ac3d1356d4bb7fb2f365cad90c84c0f8573e 100644 --- a/include/class.search.php +++ b/include/class.search.php @@ -670,8 +670,8 @@ class SavedSearch extends CustomQueue { return count($errors) === 0; } - static function create() { - $search = parent::create(); + static function create($vars=false) { + $search = parent::create($vars); $search->clearFlag(self::FLAG_QUEUE); return $search; } @@ -833,7 +833,7 @@ class AssigneeChoiceField extends ChoiceField { } class AgentSelectionField extends ChoiceField { - function getChoices() { + function getChoices($verbose=false) { return Staff::getStaffMembers(); } @@ -858,7 +858,7 @@ class AgentSelectionField extends ChoiceField { } class TeamSelectionField extends ChoiceField { - function getChoices() { + function getChoices($verbose=false) { return Team::getTeams(); } diff --git a/include/i18n/en_US/queue.yaml b/include/i18n/en_US/queue.yaml index 4d01f5fbb8c57c8b9ab1add551e075a98ae25535..678575ba2846e5de8e35efca2e70e785fa253e4b 100644 --- a/include/i18n/en_US/queue.yaml +++ b/include/i18n/en_US/queue.yaml @@ -15,8 +15,8 @@ # 0x02: FLAG_QUEUE (should be set for everything here) # 0x04: FLAG_CONTAINER (should be set for top-level queues) # 0x08: FLAG_INHERIT (inherit criteria from parent) -# 0x10: FLAG_DEFAULT (default queue for parent container) -# 0x20: FLAG_DRAFT +# 0x10: FLAG_INHERIT_COLUMNS +# 0x20: FLAG_INHERIT_SORTING # staff_id: User owner of the queue # sort: Manual sort order # title: Display name of the queue @@ -58,6 +58,11 @@ sort: 6 width: 160 heading: Assigned To + sorts: + - sort_id: 1 + - sort_id: 2 + - sort_id: 3 + - sort_id: 4 - id: 2 title: Closed @@ -93,7 +98,7 @@ - title: Unanswered parent_id: 1 - flags: 0x0b + flags: 0x2b root: T sort: 1 config: '[["isanswered","nset",null]]' @@ -125,7 +130,7 @@ - title: Unassigned parent_id: 1 - flags: 0x0b + flags: 0x2b root: T sort: 2 config: '[["assignee","!assigned",null]]' @@ -157,7 +162,7 @@ - title: My Tickets parent_id: 1 - flags: 0x0b + flags: 0x2b root: T sort: 4 config: '[["assignee","includes",{"M":"Me", "T":"One of my teams"}]]' diff --git a/include/i18n/en_US/queue_sort.yaml b/include/i18n/en_US/queue_sort.yaml new file mode 100644 index 0000000000000000000000000000000000000000..34133fc7a532804b42073cb066119abca969e0f4 --- /dev/null +++ b/include/i18n/en_US/queue_sort.yaml @@ -0,0 +1,21 @@ +# Columns are not necessary and a default list is used if no columns are +# specified. +# +# Fields: +# id: +--- +- id: 1 + name: Priority + Most Recently Updated + columns: '["-cdata__priority","-lastupdate"]' + +- id: 2 + name: Priority + Most Recently Created + columns: '["-cdata__priority","-created"]' + +- id: 3 + name: Priority + Due Date + columns: '["-cdata__priority","-est_duedate"]' + +- id: 4 + name: Due Date + columns: '["-est_duedate"]' diff --git a/include/staff/queue.inc.php b/include/staff/queue.inc.php index 39f0f8007539e5c36eef6f1cd1c828c179aabfa5..63db07b90cf9687c8b21646acd33283087ecfcdd 100644 --- a/include/staff/queue.inc.php +++ b/include/staff/queue.inc.php @@ -146,52 +146,126 @@ else { <br><?php echo __("Add, edit or remove the sorting criteria for this custom queue using the options below. Sorting is priortized in ascending order."); ?></p> </div> <table class="queue-sort table"> - <tbody class="sortable-rows ui-sortable"> - <tr style="display: table-row;"> - <td> - <i class="faded-more icon-sort"></i> - <a class="inline" - href="#" onclick="javascript: - var colid = $(this).closest('tr').find('[data-name=sorting_id]').val(); - $.dialog('ajax.php/tickets/search/sorting/edit/' + colid, 201); - return false;"><?php echo __('This is sort criteria title 1'); ?></a> - </td> - <td> - <a href="#" class="pull-right drop-column" title="Delete"><i class="icon-trash"></i></a> - </td> - </tr> - <tr style="display: table-row;"> - <td> - <i class="faded-more icon-sort"></i> - <a class="inline" - href="#" onclick="javascript: - var colid = $(this).closest('tr').find('[data-name=sorting_id]').val(); - $.dialog('ajax.php/tickets/search/sorting/edit/' + colid, 201); - return false; - "><?php echo __('This is sort criteria title 2'); ?></a> - </td> - <td> - <a href="#" class="pull-right drop-column" title="Delete"><i class="icon-trash"></i></a> - </td> - </tr> - </tbody> - <tbody> - <tr class="header"> - <td colspan="3"></td> - </tr> - <tr> - <td colspan="3" id="append-sort"> - <i class="icon-plus-sign"></i> - <select id="add-sort" data-quick-add="queue-column"> - <option value="">— Add Sort Criteria —</option> - <option value="">Sort Option 1</option> - <option value="">Sort Option 2</option> - <option value="0" data-quick-add>— <?php echo __('Add New Sort Criteria');?> —</option> - </select> - <button type="button" class="green button">Add</button> - </td> - </tr> +<?php +if ($queue->parent) { ?> + <tbody> + <tr> + <td colspan="3"> + <input type="checkbox" name="inherit-sorting" <?php + if ($queue->inheritSorting()) echo 'checked="checked"'; ?> + onchange="javascript:$(this).closest('table').find('.if-not-inherited').toggle(!$(this).prop('checked'));" /> + <?php echo __('Inherit sorting from the parent queue'); ?> + <br /><br /> + </td> + </tr> + </tbody> +<?php } ?> + <tbody class="if-not-inherited <?php if ($queue->inheritSorting()) echo 'hidden'; ?>"> + <tr class="header"> + <td nowrap><small><b><?php echo __('Name'); ?></b></small></td> + <td><small><b><?php echo __('Details'); ?></b></small></td> + <td/> + </tr> + </tbody> + <tbody class="sortable-rows if-not-inherited <?php + if ($queue->inheritSorting()) echo 'hidden'; ?>"> + <tr id="sort-template" class="hidden"> + <td nowrap> + <i class="faded-more icon-sort"></i> + <input type="hidden" data-name="sorts[]" /> + <span data-name="name"></span> + </td> + <td> + <div> + <a class="inline action-button" + href="#" onclick="javascript: + var colid = $(this).closest('tr').find('[data-name^=sorts]').val(); + $.dialog('ajax.php/tickets/search/sort/edit/' + colid, 201); + return false; + "><i class="icon-cog"></i> <?php echo __('Config'); ?></a> + </div> + </td> + <td> + <a href="#" class="pull-right drop-sort" title="<?php echo __('Delete'); + ?>"><i class="icon-trash"></i></a> + </td> + <tr> + </tbody> + <tbody class="if-not-inherited <?php + if ($queue->inheritSorting()) echo 'hidden'; ?>"> + <tr class="header"> + <td colspan="3"></td> + </tr> + <tr> + <td colspan="3" id="append-sort"> + <i class="icon-plus-sign"></i> + <select id="add-sort" data-quick-add="queue-sort"> + <option value="">— <?php + echo __('Add Sort Criteria'); ?> —</option> +<?php foreach (QueueSort::forQueue($queue) as $QS) { ?> + <option value="<?php echo $QS->id; ?>"><?php + echo Format::htmlchars($QS->name); ?></option> +<?php } ?> + <option value="0" data-quick-add>— <?php + echo __('Add New Sort Criteria');?> —</option> + </select> + <button type="button" class="green button"><?php + echo __('Add'); ?></button> + </td> + </tr> </tbody> +<script> ++function() { +var Q = setInterval(function() { + if ($('#append-sort').length == 0) + return; + clearInterval(Q); + + var addSortOption = function(sortid, info) { + if (!sortid) return; + var copy = $('#sort-template').clone(); + info['sorts[]'] = sortid; + copy.find('input[data-name]').each(function() { + var $this = $(this), + name = $this.data('name'); + if (info[name] !== undefined) { + $this.val(info[name]); + } + $this.attr('name', name); + }); + copy.find('span').text(info['name']); + copy.attr('id', '').show().insertBefore($('#sort-template')); + copy.removeClass('hidden'); + copy.find('a.drop-sort').click(function() { + $('<option>') + .attr('value', copy.find('input[data-name^=sorts]').val()) + .text(info.name) + .insertBefore($('#add-sort') + .find('[data-quick-add]') + ); + copy.fadeOut(function() { $(this).remove(); }); + return false; + }); + var selected = $('#add-sort').find('option[value=' + sortid + ']'); + selected.remove(); + }; + + $('#append-sort').find('button').on('click', function() { + var selected = $('#add-sort').find(':selected'), + id = parseInt(selected.val()); + if (!id) + return; + addSortOption(id, {name: selected.text()}); + return false; + }); +<?php foreach ($queue->getSortOptions() as $C) { + echo sprintf('addSortOption(%d, {name: %s});', + $C->sort_id, JsonDataEncoder::encode($C->getName()) + ); +} ?> +}, 25); +}(); +</script> </table> </div> diff --git a/include/staff/templates/queue-sort.tmpl.php b/include/staff/templates/queue-sort.tmpl.php index 42b56437057c208450bfbc3c29ffc89e2732ced6..5c2a14e63b2d4045f62b56daeb8fbdd5201214e3 100644 --- a/include/staff/templates/queue-sort.tmpl.php +++ b/include/staff/templates/queue-sort.tmpl.php @@ -1,29 +1,43 @@ +<?php +if (count($queue->getSortOptions()) === 0) + return; -<span class="action-button muted" data-dropdown="#sort-dropdown" data-toggle="tooltip" title="<?php echo $sort_options[$sort_cols]; ?>"> +if (strpos($_GET['sort'], 'qs-') === 0) { + $sort_id = substr($_GET['sort'], 3); + $queuesort = QueueSort::lookup($sort_id); +} + +$sort_dir = $_GET['dir']; +?> + +<span class="action-button muted" data-dropdown="#sort-dropdown" + data-toggle="tooltip" title="<?php + if (is_object($queuesort)) echo Format::htmlchars($queuesort->getName()); ?>"> <i class="icon-caret-down pull-right"></i> <span><i class="icon-sort-by-attributes-alt <?php if ($sort_dir) echo 'icon-flip-vertical'; ?>"></i> <?php echo __('Sort');?></span> </span> <div id="sort-dropdown" class="action-dropdown anchor-right" onclick="javascript: -var query = addSearchParam({'sort': $(event.target).data('mode'), 'dir': $(event.target).data('dir')}); +var $et = $(event.target), + query = addSearchParam({'sort': $et.data('mode'), 'dir': $et.data('dir')}); $.pjax({ url: '?' + query, timeout: 2000, container: '#pjax-container'});"> <ul class="bleed-left"> - <?php foreach ($queue_sort_options as $mode) { - $desc = $sort_options[$mode]; + <?php foreach ($queue->getSortOptions() as $qs) { + $desc = $qs->getName(); $icon = ''; $dir = '0'; - $selected = $sort_cols == $mode; ?> + $selected = isset($queuesort) && $queuesort->id == $qs->id; ?> <li <?php if ($selected) { - echo 'class="active"'; - $dir = ($sort_dir == '1') ? '0' : '1'; // Flip the direction - $icon = ($sort_dir == '1') ? 'icon-hand-up' : 'icon-hand-down'; + echo 'class="active"'; + $dir = ($sort_dir == '1') ? '0' : '1'; // Flip the direction + $icon = ($sort_dir == '1') ? 'icon-hand-up' : 'icon-hand-down'; } ?>> - <a href="#" data-mode="<?php echo $mode; ?>" data-dir="<?php echo $dir; ?>"> + <a href="#" data-mode="qs-<?php echo $qs->id; ?>" data-dir="<?php echo $dir; ?>"> <i class="icon-fixed-width <?php echo $icon; ?>" ></i> <?php echo Format::htmlchars($desc); ?></a> </li> diff --git a/include/staff/templates/queue-sorting-add.tmpl.php b/include/staff/templates/queue-sorting-add.tmpl.php index ab5c345a8a907a4bc37a2e64350bd099a840f37e..4fcc3b8ff1dcbb73374cae050390821cee698068 100644 --- a/include/staff/templates/queue-sorting-add.tmpl.php +++ b/include/staff/templates/queue-sorting-add.tmpl.php @@ -6,11 +6,11 @@ */ $colid = 0; ?> -<h3 class="drag-handle"><?php echo __('Add Sort Options'); ?></h3> +<h3 class="drag-handle"><?php echo __('Add Sort Option'); ?></h3> <a class="close" href=""><i class="icon-remove-circle"></i></a> <hr/> -<form method="post" action="#admin/quick-add/queue-column"> +<form method="post" action="#admin/quick-add/queue-sort"> <?php include 'queue-sorting.tmpl.php'; diff --git a/include/staff/templates/queue-sorting-edit.tmpl.php b/include/staff/templates/queue-sorting-edit.tmpl.php index 0e2734d4822d618842fcaacbd49770685db099d0..0a2d98b0246433b59ea0f9cc1d575deb3a4ed41b 100644 --- a/include/staff/templates/queue-sorting-edit.tmpl.php +++ b/include/staff/templates/queue-sorting-edit.tmpl.php @@ -4,15 +4,15 @@ * * $column - <QueueColumn> instance for this column */ -$colid = $column->getId(); +$sortid = $sort->getId(); ?> <h3 class="drag-handle"><?php echo __('Manage Sort Options'); ?> — - <?php echo $column->get('name') ?></h3> + <?php echo $sort->get('name') ?></h3> <a class="close" href=""><i class="icon-remove-circle"></i></a> <hr/> -<form method="post" action="#tickets/search/column/edit/<?php - echo $colid; ?>"> +<form method="post" action="#tickets/search/sort/edit/<?php + echo $sortid; ?>"> <?php include 'queue-sorting.tmpl.php'; diff --git a/include/staff/templates/queue-sorting.tmpl.php b/include/staff/templates/queue-sorting.tmpl.php index d73c0f9c2bb6b7adc3871e22cf7bb927fb1b90b3..2640e43acfed9c1c9ad917e2d632cf57dec63b1b 100644 --- a/include/staff/templates/queue-sorting.tmpl.php +++ b/include/staff/templates/queue-sorting.tmpl.php @@ -1,67 +1,106 @@ -<div class="tab-desc"> - <p><b>Manage Custom Sorting</b> - <br>Add, and remove the fields in this list using the options below. Sorting is priortized in ascending order.</p> -</div> +<?php echo $sort->getDataConfigForm()->asTable(); ?> + <table class="table"> - <tbody> - <tr> - <td colspan="3" style="border-bottom:1px"> - <input type="text" name="name" value="" style="width:100%" placeholder="<?php echo __('Sort Criteria Title');?>" /> - </td> - </tr> - </tbody> - <tbody class="sortable-rows ui-sortable"> - <tr style="display: table-row;"> - <td> - <i class="faded-more icon-sort"></i> - <span><?php echo __('Sort field 0'); ?></span> - </td> - <td> - <select> - <option value="0"> - <?php echo __('Ascending');?> - </option> - <option value="1"> - <?php echo __('Descending');?> - </option> - </select> - </td> - <td> - <a href="#" class="pull-right drop-column" title="Delete"><i class="icon-trash"></i></a> - </td> - </tr> - <tr style="display: table-row;"> - <td> - <i class="faded-more icon-sort"></i> - <span><?php echo __('Sort field 1'); ?></span> - </td> - <td> - <select> - <option value="0"> - <?php echo __('Ascending');?> - </option> - <option value="1"> - <?php echo __('Descending');?> - </option> - </select> - </td> - <td> - <a href="#" class="pull-right drop-column" title="Delete"><i class="icon-trash"></i></a> - </td> - </tr> + <tbody class="sortable-rows"> + <tr id="sort-column-template" class="hidden"> + <td nowrap> + <i class="faded-more icon-sort"></i> + <span data-name="label"></span> + </td> + <td> + <select data-name="descending"> + <option value="0"> + <?php echo __('Ascending');?> + </option> + <option value="1"> + <?php echo __('Descending');?> + </option> + </select> + </td> + <td> + <a href="#" class="pull-right drop-column" title="Delete"><i class="icon-trash"></i></a> + </td> + </tr> </tbody> <tbody> <tr class="header"> <td colspan="3"></td> </tr> <tr> - <td colspan="3" id="append-sort"> + <td colspan="3" id="append-sort-column"> <i class="icon-plus-sign"></i> - <select id="add-sort"> - <option value="">— Add Field —</option> + <select id="add-sort-column"> + <option value="">— <?php echo __("Add Field"); ?> —</option> +<?php foreach (CustomQueue::getSearchableFields($sort->getRoot()) as $path=>$F) { + list($label,) = $F; +?> + <option value="<?php echo Format::htmlchars($path); ?>"><?php + echo Format::htmlchars($label); + ?></option> +<?php } ?> </select> - <button type="button" class="green button">Add</button> + <button type="button" class="green button"><?php + echo __('Add'); ?></button> </td> </tr> </tbody> -</table> \ No newline at end of file +<script> ++function() { +var Q = setInterval(function() { + if ($('#append-sort-column').length == 0) + return; + clearInterval(Q); + + var addSortColumn = function(info) { + if (!info.path) return; + var copy = $('#sort-column-template').clone(), + name_prefix = 'columns[' + info.path + ']'; + copy.find(':input[data-name]').each(function() { + var $this = $(this), + name = $this.data('name'); + + if (info[name] !== undefined) { + if ($this.is(':checkbox')) + $this.prop('checked', info[name]); + else + $this.val(info[name]); + } + $this.attr('name', name_prefix + '[' + name + ']'); + }); + copy.find('span').text(info['name']); + copy.attr('id', '').show().insertBefore($('#sort-column-template')); + copy.removeClass('hidden'); + copy.find('a.drop-column').click(function() { + $('<option>') + .attr('value', copy.find('input[data-name=column_id]').val()) + .text(info.name) + .insertBefore($('#add-column') + .find('[data-quick-add]') + ); + copy.fadeOut(function() { $(this).remove(); }); + return false; + }); + var selected = $('#add-sort-column').find('option[value=' + info.path + ']'); + selected.remove(); + }; + + $('#append-sort-column').find('button').on('click', function() { + var selected = $('#add-sort-column').find(':selected'), + path = selected.val(); + if (!path) + return; + addSortColumn({path: path, name: selected.text(), descending: 0}); + return false; + }); +<?php foreach ($sort->getColumns() as $path=>$C) { + list(list($label,), $descending) = $C; + echo sprintf('addSortColumn({path: %s, name: %s, descending: %d});', + JsonDataEncoder::encode($path), + JsonDataEncoder::encode($label), + $descending ? 1 : 0 + ); +} ?> +}, 25); +}(); +</script> +</table> diff --git a/include/staff/templates/queue-tickets.tmpl.php b/include/staff/templates/queue-tickets.tmpl.php index 50f94fab984f046d999ea614c2c36a48b02b1443..83ff38e0a07a91a744a7851c3691ae4800174c72 100644 --- a/include/staff/templates/queue-tickets.tmpl.php +++ b/include/staff/templates/queue-tickets.tmpl.php @@ -162,12 +162,24 @@ if ($canManageTickets) { ?> <th style="width:12px"></th> <?php } -if (isset($_GET['sort'])) { +if (isset($_GET['sort']) && is_numeric($_GET['sort'])) { $sort = $_SESSION['sort'][$queue->getId()] = array( 'col' => (int) $_GET['sort'], 'dir' => (int) $_GET['dir'], ); } +elseif (isset($_GET['sort']) + // Drop the leading `qs-` + && (strpos($_GET['sort'], 'qs-') === 0) + && ($sort_id = substr($_GET['sort'], 3)) + && is_numeric($sort_id) + && ($sort = QueueSort::lookup($sort_id)) +) { + $sort = $_SESSION['sort'][$queue->getId()] = array( + 'queuesort' => $sort, + 'dir' => (int) $_GET['dir'], + ); +} else { $sort = $_SESSION['sort'][$queue->getId()]; } @@ -188,7 +200,11 @@ foreach ($columns as $C) { if (isset($sort['col']) && $sort['col'] == $C->id) { $tickets = $C->applySort($tickets, $sort['dir']); } -} ?> +} +if (isset($sort['queuesort'])) { + $sort['queuesort']->applySort($tickets, $sort['dir']); +} +?> </tr> </thead> <tbody> diff --git a/include/upgrader/streams/core.sig b/include/upgrader/streams/core.sig index 5c87bd1bf8de944497a1e5a4357e9a26722bdeb9..482fa87699383c4d90d2424748e47fd0344c75c0 100644 --- a/include/upgrader/streams/core.sig +++ b/include/upgrader/streams/core.sig @@ -1 +1 @@ -934b8db8f97d6859d013b6219957724f +a099b35b5ed141de6213f165b197e623 diff --git a/include/upgrader/streams/core/934b8db8-a099b35b.patch.sql b/include/upgrader/streams/core/934b8db8-a099b35b.patch.sql new file mode 100644 index 0000000000000000000000000000000000000000..4a3b26006f6fd73d539580fef381b16dbfe1ea2e --- /dev/null +++ b/include/upgrader/streams/core/934b8db8-a099b35b.patch.sql @@ -0,0 +1,35 @@ +/** + * @version v1.11 + * @signature a099b35b5ed141de6213f165b197e623 + * @title Custom Queues, Advanced Sorting + * + * Add advanced sorting configuration to custom queues + */ + +ALTER TABLE `%TABLE_PREFIX%queue` + ADD `sort_id` int(11) unsigned AFTER `columns_id`; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%queue_sort`; +CREATE TABLE `%TABLE_PREFIX%queue_sort` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `root` varchar(32) DEFAULT NULL, + `name` varchar(64) NOT NULL DEFAULT '', + `columns` text, + `updated` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%queue_sorts`; +CREATE TABLE `%TABLE_PREFIX%queue_sorts` ( + `queue_id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `sort_id` int(11) unsigned NOT NULL, + `bits` int(11) unsigned NOT NULL DEFAULT '0', + `sort` int(10) unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`queue_id`) +) DEFAULT CHARSET=utf8; + +-- Finished with patch +UPDATE `%TABLE_PREFIX%config` + SET `value` = 'a099b35b5ed141de6213f165b197e623' + WHERE `key` = 'schema_signature' AND `namespace` = 'core'; + diff --git a/include/upgrader/streams/core/934b8db8-a099b35b.task.php b/include/upgrader/streams/core/934b8db8-a099b35b.task.php new file mode 100644 index 0000000000000000000000000000000000000000..aef435f94881e4fe5cb7020a0fdc5146281f0fdc --- /dev/null +++ b/include/upgrader/streams/core/934b8db8-a099b35b.task.php @@ -0,0 +1,25 @@ +<?php + +class QueueSortCreator extends MigrationTask { + var $description = "Load customziable sorting for ticket queues"; + + function run($time) { + $i18n = new Internationalization('en_US'); + $columns = $i18n->getTemplate('queue_sort.yaml')->getData(); + foreach ($columns as $C) { + QueueSort::__create($C); + } + + $open = CustomQueue::lookup(1); + foreach (QueueSort::forQueue($open) as $qs) { + $open->sorts->add($qs); + } + $open->sorts->saveAll(); + + foreach ($open->getChildren() as $q) { + $q->flags |= CustomQueue::FLAG_INHERIT_SORTING; + $q->save(); + } + } +} +return 'QueueSortCreator'; diff --git a/scp/ajax.php b/scp/ajax.php index 0b4b015a0aa2fc0b8342ca61f912f269aae0a8ba..570020809037fb6e0222d9b0717b736db4ebaf71 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -176,6 +176,7 @@ $dispatcher = patterns('', url_post('^/create$', 'createSearch'), url_get('^/field/(?P<id>[\w_!:]+)$', 'addField'), url('^/column/edit/(?P<id>\d+)$', 'editColumn'), + url('^/sort/edit/(?P<id>\d+)$', 'editSort'), url_post('^(?P<id>\d+)/delete$', 'deleteQueues'), url_post('^(?P<id>\d+)/disable$', 'disableQueues'), url_post('^(?P<id>\d+)/enable$', 'undisableQueues') @@ -247,7 +248,8 @@ $dispatcher = patterns('', url('^/team$', 'addTeam'), url('^/role$', 'addRole'), url('^/staff$', 'addStaff'), - url('^/queue-column$', 'addQueueColumn') + url('^/queue-column$', 'addQueueColumn'), + url('^/queue-sort$', 'addQueueSort') )), url_get('^/role/(?P<id>\d+)/perms', 'getRolePerms') )), diff --git a/scp/css/scp.css b/scp/css/scp.css index 3a6b184c749aafa8c96cef18a8d3f40d68062027..9df71e3cc556af2852c3949d9f8e699d66a1b9c4 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -1269,9 +1269,6 @@ a.print { padding: 4px; background-color:#fff; } -.queue-sort.table td:not(:empty) { - padding: 10px; -} .table.two-column tbody tr td:first-child { width: 25%; } diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql index 28ed813984c86f132f73275f61182a7323ac7637..78c51e1b7cda3e370bba3ab0fdb291f1976d6be8 100644 --- a/setup/inc/streams/core/install-mysql.sql +++ b/setup/inc/streams/core/install-mysql.sql @@ -828,6 +828,7 @@ CREATE TABLE `%TABLE_PREFIX%queue` ( `id` int(11) unsigned not null auto_increment, `parent_id` int(11) unsigned not null default 0, `columns_id` int(11) unsigned default null, + `sort_id` int(11) unsigned default null, `flags` int(11) unsigned not null default 0, `staff_id` int(11) unsigned not null default 0, `sort` int(11) unsigned not null default 0, @@ -867,6 +868,25 @@ CREATE TABLE `%TABLE_PREFIX%queue_columns` ( PRIMARY KEY (`queue_id`, `column_id`) ) DEFAULT CHARSET=utf8; +DROP TABLE IF EXISTS `%TABLE_PREFIX%queue_sort`; +CREATE TABLE `%TABLE_PREFIX%queue_sort` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `root` varchar(32) DEFAULT NULL, + `name` varchar(64) NOT NULL DEFAULT '', + `columns` text, + `updated` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%queue_sorts`; +CREATE TABLE `%TABLE_PREFIX%queue_sorts` ( + `queue_id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `sort_id` int(11) unsigned NOT NULL, + `bits` int(11) unsigned NOT NULL DEFAULT '0', + `sort` int(10) unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`queue_id`) +) DEFAULT CHARSET=utf8; + DROP TABLE IF EXISTS `%TABLE_PREFIX%translation`; CREATE TABLE `%TABLE_PREFIX%translation` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT,