diff --git a/modules/backend/behaviors/ListController.php b/modules/backend/behaviors/ListController.php
index e69326e8ef..b1dd201c6a 100644
--- a/modules/backend/behaviors/ListController.php
+++ b/modules/backend/behaviors/ListController.php
@@ -5,6 +5,7 @@
use Flash;
use ApplicationException;
use Backend\Classes\ControllerBehavior;
+use Winter\Storm\Database\Traits\HasSortableRelations;
/**
* Adds features for working with backend lists.
@@ -150,6 +151,7 @@ public function makeList($definition = null)
'showCheckboxes',
'showTree',
'treeExpanded',
+ 'sortable',
'customViewPath',
];
@@ -431,6 +433,22 @@ public function listRefresh(string $definition = null)
return $this->listWidgets[$definition]->onRefresh();
}
+ /**
+ * Returns the sort order value for a specific record.
+ */
+ public function getRecordSortOrder($record, $relation = '')
+ {
+ if ($relation) {
+ /** @var HasSortableRelations $modelInstance */
+ $modelInstance = new $this->config->modelClass;
+ $reorderColumn = $modelInstance->getRelationSortOrderColumn($relation);
+
+ return $this->controller->asExtension('ReorderRelationController')->getRelationRecordSortOrder($record, $reorderColumn);
+ }
+
+ return $record->{$record->getSortOrderColumn()};
+ }
+
/**
* Returns the widget used by this behavior.
* @return \Backend\Classes\WidgetBase
diff --git a/modules/backend/behaviors/RelationController.php b/modules/backend/behaviors/RelationController.php
index 9e0f221ed8..f77c660114 100644
--- a/modules/backend/behaviors/RelationController.php
+++ b/modules/backend/behaviors/RelationController.php
@@ -667,6 +667,10 @@ protected function makeViewWidget()
$config->alias = $this->alias . 'ViewList';
$config->showSorting = $this->getConfig('view[showSorting]', true);
$config->defaultSort = $this->getConfig('view[defaultSort]');
+ $config->sortable = $this->getConfig('view[sortable]', false);
+ $config->reorderRelation = $this->relationName;
+ $config->reorderModel = get_class($this->model);
+ $config->reorderParentId = $this->model->getKey();
$config->recordsPerPage = $this->getConfig('view[recordsPerPage]');
$config->showCheckboxes = $this->getConfig('view[showCheckboxes]', !$this->readOnly);
$config->recordUrl = $this->getConfig('view[recordUrl]');
diff --git a/modules/backend/behaviors/ReorderRelationController.php b/modules/backend/behaviors/ReorderRelationController.php
new file mode 100644
index 0000000000..9740ffe280
--- /dev/null
+++ b/modules/backend/behaviors/ReorderRelationController.php
@@ -0,0 +1,245 @@
+config = [];
+ if ($controller->reorderRelationConfig) {
+ $this->config = $this->makeConfig($controller->reorderRelationConfig, []);
+ }
+ }
+
+ //
+ // AJAX
+ //
+
+ /**
+ * Updates the relation order.
+ * @throws \Exception
+ */
+ public function onReorderRelation()
+ {
+ $this->reorderRelationGetModel();
+ $this->validateModel();
+
+ if (
+ (!$ids = post('record_ids')) ||
+ (!$orders = post('sort_orders'))
+ ) {
+ return;
+ }
+
+ /** @var HasSortableRelations $instance */
+ $instance = $this->parentModel->newQuery()->find($this->postValue('_reorder_parent_id'));
+ $instance->setRelationOrder($this->relation, $ids, $orders);
+
+ // refresh the relation view after reorder
+ $this->controller->initRelation(
+ $this->parentModel->newQuery()->findOrFail($this->postValue('_reorder_parent_id')),
+ $this->relation
+ );
+ return $this->controller->relationRefresh($this->relation);
+ }
+
+ //
+ // Reordering
+ //
+
+ /**
+ * Sets all required model properties.
+ */
+ public function reorderRelationGetModel()
+ {
+ $this->parentModel = $this->reorderRelationGetParentModel();
+ $this->relation = post('_reorder_relation_name');
+
+ $relationModelClass = array_get($this->parentModel->getRelationDefinition($this->relation), 0);
+ if (!$relationModelClass) {
+ throw new ApplicationException(
+ sprintf('Could not determine model class for relation "%s"', $this->relation)
+ );
+ }
+
+ return $this->model = new $relationModelClass;
+ }
+
+ /**
+ * Returns all the records from the supplied model.
+ * @return Collection
+ */
+ protected function getRecords()
+ {
+ $query = $this->parentModel->newQuery();
+
+ $this->controller->reorderExtendRelationQuery($query);
+
+ return $query
+ ->with([$this->relation => function ($q) {
+ $q->orderBy($this->parentModel->getRelationSortOrderColumn($this->relation), 'ASC');
+ }])
+ ->findOrFail($this->postValue('_reorder_parent_id'))
+ ->{$this->relation};
+ }
+
+ /**
+ * Extend the relation query used for finding reorder records. Extra conditions
+ * can be applied to the query, for example, $query->withTrashed();
+ * @param Winter\Storm\Database\Builder $query
+ * @return void
+ */
+ public function reorderExtendRelationQuery($query)
+ {
+ }
+
+ //
+ // Helpers
+ //
+
+ /**
+ * Prepares common partial variables.
+ */
+ protected function prepareVars()
+ {
+ $this->vars['reorderRecords'] = $this->getRecords();
+ $this->vars['reorderModel'] = $this->model;
+ }
+
+ /**
+ * Return a model instance based on the _reorder_model post value.
+ * @return Model
+ */
+ public function reorderRelationGetParentModel()
+ {
+ $model = $this->postValue('_reorder_model');
+
+ if (!class_exists($model)) {
+ throw new ApplicationException(
+ sprintf('Model class "%s" does not exist', $model)
+ );
+ }
+
+ return new $model;
+ }
+
+ public function getRelationRecordSortOrder($record, $sortColumn)
+ {
+ return $record->pivot ? $record->pivot->{$sortColumn} : $record->{$sortColumn};
+ }
+
+ /**
+ * Validate the supplied form model.
+ * @return void
+ */
+ protected function validateModel()
+ {
+ $modelTraits = class_uses($this->parentModel);
+
+ if (!isset($modelTraits[\Winter\Storm\Database\Traits\HasSortableRelations::class])) {
+ throw new ApplicationException(
+ sprintf('The "%s" model must implement the HasSortableRelations trait.', get_class($this->parentModel))
+ );
+ }
+ }
+
+ /**
+ * Returns the name attribute for a given $record. The attribute
+ * defined in the behaviour config is used here.
+ *
+ * @param Model $record
+ * @param $relation
+ *
+ * @return string
+ */
+ public function reorderRelationGetRecordName(Model $record, $relation)
+ {
+ $attribute = array_get((array)$this->config, "$relation.nameFrom");
+ if ($attribute) {
+ return (string)$record->$attribute;
+ }
+
+ // Take a guess if no "nameFrom" config is set.
+ return (string)($record->name ?: $record->title);
+ }
+
+ /**
+ * Controller accessor for making partials within this behavior.
+ * @param string $partial
+ * @param array $params
+ * @return string Partial contents
+ */
+ public function reorderRelationMakePartial($partial, $params = [])
+ {
+ $contents = $this->controller->makePartial(
+ 'reorder_' . $partial,
+ $params + $this->vars,
+ false
+ );
+
+ if (!$contents) {
+ $contents = $this->makePartial($partial, $params);
+ }
+
+ return $contents;
+ }
+
+ /**
+ * Fetch a post value for the current relation.
+ *
+ * @param string $key
+ *
+ * @return mixed
+ */
+ private function postValue(string $key)
+ {
+ $relation = post('_reorder_relation_name');
+
+ return post($key. '.' . $relation);
+ }
+}
diff --git a/modules/backend/behaviors/relationcontroller/partials/_button_reorder.htm b/modules/backend/behaviors/relationcontroller/partials/_button_reorder.htm
new file mode 100644
index 0000000000..c1b7f4f70c
--- /dev/null
+++ b/modules/backend/behaviors/relationcontroller/partials/_button_reorder.htm
@@ -0,0 +1,14 @@
+vars['relationField']; ?>
+
+ = e(trans('backend::lang.reorder.default_title')) ?>
+
diff --git a/modules/backend/behaviors/reordercontroller/assets/js/winter.reorder.js b/modules/backend/behaviors/reordercontroller/assets/js/winter.reorder.js
index e3b5035352..023beefda8 100644
--- a/modules/backend/behaviors/reordercontroller/assets/js/winter.reorder.js
+++ b/modules/backend/behaviors/reordercontroller/assets/js/winter.reorder.js
@@ -6,15 +6,16 @@
* - Nested sorting: Post back source and target nodes IDs and the move positioning.
*/
+function ($) { "use strict";
-
var ReorderBehavior = function() {
this.sortMode = null
+ this.context = 'default'
this.simpleSortOrders = []
- this.initSorting = function (mode) {
+ this.initSorting = function (mode, context) {
this.sortMode = mode
+ this.context = context
if (mode == 'simple') {
this.initSortingSimple()
@@ -34,7 +35,9 @@
postData = this.getNestedMoveData(sortData)
}
- $('#reorderTreeList').request('onReorder', {
+ var handler = this.context === 'relation' ? 'onReorderRelation' : 'onReorder'
+
+ $('#reorderTreeList').request(handler, {
data: postData
})
}
@@ -79,4 +82,4 @@
}
$.wn.reorderBehavior = new ReorderBehavior;
-}(window.jQuery);
\ No newline at end of file
+}(window.jQuery);
diff --git a/modules/backend/behaviors/reorderrelationcontroller/partials/_container.htm b/modules/backend/behaviors/reorderrelationcontroller/partials/_container.htm
new file mode 100644
index 0000000000..bb3e6ec93b
--- /dev/null
+++ b/modules/backend/behaviors/reorderrelationcontroller/partials/_container.htm
@@ -0,0 +1,30 @@
+
+= Form::open() ?>
+
+
+
+
+
+
+
+
+ = $this->reorderRelationMakePartial('records', [
+ 'records' => $reorderRecords,
+ 'sortColumn' => $reorderSortColumn,
+ 'relation' => $reorderRelation
+ ]) ?>
+
+
+
= Lang::get('backend::lang.reorder.no_records') ?>
+
+
+= Form::close() ?>
+
+
diff --git a/modules/backend/behaviors/reorderrelationcontroller/partials/_records.htm b/modules/backend/behaviors/reorderrelationcontroller/partials/_records.htm
new file mode 100644
index 0000000000..b25d939efe
--- /dev/null
+++ b/modules/backend/behaviors/reorderrelationcontroller/partials/_records.htm
@@ -0,0 +1,13 @@
+
+
+
+
+
+
= e($this->reorderRelationGetRecordName($record, $relation)) ?>
+
+
+
+
+
diff --git a/modules/backend/behaviors/reorderrelationcontroller/partials/_relation_modal.htm b/modules/backend/behaviors/reorderrelationcontroller/partials/_relation_modal.htm
new file mode 100644
index 0000000000..8df704ad9d
--- /dev/null
+++ b/modules/backend/behaviors/reorderrelationcontroller/partials/_relation_modal.htm
@@ -0,0 +1,28 @@
+
+
+
+ = $container ?>
+
+
+
diff --git a/modules/backend/behaviors/reorderrelationcontroller/partials/_request_params.htm b/modules/backend/behaviors/reorderrelationcontroller/partials/_request_params.htm
new file mode 100644
index 0000000000..fd580ed9c6
--- /dev/null
+++ b/modules/backend/behaviors/reorderrelationcontroller/partials/_request_params.htm
@@ -0,0 +1,6 @@
+data-request="onRelationModalClose"
+data-request-data="
+ _reorder_relation_name: '= e($reorderRelation) ?>',
+ _reorder_model[= e($reorderRelation) ?>]: '= e(addslashes($reorderModel)) ?>',
+ _reorder_parent_id[= e($reorderRelation) ?>]: '= e($reorderParentId) ?>',
+"
diff --git a/modules/backend/lang/de/lang.php b/modules/backend/lang/de/lang.php
index 9c8148c1a5..8e4e11e77f 100644
--- a/modules/backend/lang/de/lang.php
+++ b/modules/backend/lang/de/lang.php
@@ -205,6 +205,7 @@
'refresh' => 'Erneuern',
'updating' => 'Aktualisiere...',
'loading' => 'Laden...',
+ 'sort_handle' => 'Sortierung',
'setup_title' => 'Listen Setup',
'setup_help' => 'Benutzen Sie Checkboxen, um Spalten auszuwählen, welche Sie in den Listen sehen möchten. Sie können die Position der Spalten ändern, indem Sie diese hinauf oder hinunter ziehen.',
'records_per_page' => 'Aufzeichnungen pro Seite',
@@ -334,7 +335,8 @@
],
'reorder' => [
'default_title' => 'Einträge sortieren',
- 'no_records' => 'Es gibt keine Einträge zum sortieren.'
+ 'no_records' => 'Es gibt keine Einträge zum sortieren.',
+ 'relation' => 'Sortiere verwandte Einträge',
],
'model' => [
'name' => "Model",
diff --git a/modules/backend/lang/en/lang.php b/modules/backend/lang/en/lang.php
index 7b29eea022..86e0ac16df 100644
--- a/modules/backend/lang/en/lang.php
+++ b/modules/backend/lang/en/lang.php
@@ -215,6 +215,7 @@
'refresh' => 'Refresh',
'updating' => 'Updating...',
'loading' => 'Loading...',
+ 'sort_handle' => 'Sort handle',
'setup_title' => 'List setup',
'setup_help' => 'Use checkboxes to select columns you want to see in the list. You can change position of columns by dragging them up or down.',
'records_per_page' => 'Records per page',
@@ -351,6 +352,7 @@
'reorder' => [
'default_title' => 'Reorder records',
'no_records' => 'There are no records available to sort.',
+ 'relation' => 'Sort related entries',
],
'model' => [
'name' => 'Model',
diff --git a/modules/backend/widgets/Lists.php b/modules/backend/widgets/Lists.php
index 0e551f54dd..acb4cc4a79 100644
--- a/modules/backend/widgets/Lists.php
+++ b/modules/backend/widgets/Lists.php
@@ -9,6 +9,8 @@
use Carbon\Carbon;
use Winter\Storm\Html\Helper as HtmlHelper;
use Winter\Storm\Router\Helper as RouterHelper;
+use Winter\Storm\Database\Traits\Sortable;
+use Winter\Storm\Database\Traits\HasSortableRelations;
use System\Helpers\DateTime as DateTimeHelper;
use System\Classes\PluginManager;
use System\Classes\MediaLibrary;
@@ -74,6 +76,11 @@ class Lists extends WidgetBase
*/
public $showSorting = true;
+ /**
+ * @var bool Makes this list sortable in-place.
+ */
+ public $sortable = false;
+
/**
* @var mixed A default sort column to look for.
*/
@@ -187,6 +194,26 @@ class Lists extends WidgetBase
*/
protected $sortDirection;
+ /**
+ * @var string Name of the relation to sort.
+ */
+ protected $reorderRelation;
+
+ /**
+ * @var string Class path of the model to sort.
+ */
+ protected $reorderModel;
+
+ /**
+ * @var string ID of the parent model (used when sorting relations).
+ */
+ protected $reorderParentId;
+
+ /**
+ * @var string Column to sort by.
+ */
+ protected $reorderColumn;
+
/**
* @var array List of CSS classes to apply to the list container element
*/
@@ -206,6 +233,10 @@ public function init()
'showPageNumbers',
'recordsPerPage',
'perPageOptions',
+ 'sortable',
+ 'reorderRelation',
+ 'reorderModel',
+ 'reorderParentId',
'showSorting',
'defaultSort',
'showCheckboxes',
@@ -233,6 +264,20 @@ public function init()
$this->validateModel();
$this->validateTree();
+
+ if ($this->sortable) {
+ $this->addJs('/modules/system/assets/ui/js/list.sortable.js', 'core');
+ $this->showSorting = false;
+ $this->showTree = false;
+
+ if ($this->reorderRelation) {
+ /** @var HasSortableRelations $modelInstance */
+ $modelInstance = new $this->reorderModel;
+ $this->reorderColumn = $modelInstance->getRelationSortOrderColumn($this->reorderRelation);
+ } else {
+ $this->reorderColumn = $this->model->getSortOrderColumn();
+ }
+ }
}
/**
@@ -267,6 +312,10 @@ public function prepareVars()
$this->vars['showPagination'] = $this->showPagination;
$this->vars['showPageNumbers'] = $this->showPageNumbers;
$this->vars['showSorting'] = $this->showSorting;
+ $this->vars['sortable'] = $this->sortable;
+ $this->vars['reorderModel'] = $this->reorderModel;
+ $this->vars['reorderRelation'] = $this->reorderRelation;
+ $this->vars['reorderParentId'] = $this->reorderParentId;
$this->vars['sortColumn'] = $this->getSortColumn();
$this->vars['sortDirection'] = $this->sortDirection;
$this->vars['showTree'] = $this->showTree;
@@ -340,6 +389,18 @@ protected function validateModel()
));
}
+ if ($this->sortable) {
+ $checkModel = $this->reorderRelation ? $this->reorderModel : $this->model;
+ $needsTrait = $this->reorderRelation ? HasSortableRelations::class : Sortable::class;
+ $modelTraits = class_uses($checkModel);
+
+ if (!isset($modelTraits[$needsTrait])) {
+ throw new ApplicationException(
+ sprintf('The "%s" model must implement the "%s" trait.', get_class($this->model), $needsTrait)
+ );
+ }
+ }
+
return $this->model;
}
@@ -543,6 +604,12 @@ public function prepareQuery()
$sortColumn = Str::snake($column->relation) . '_count';
}
+ // Fix the sort order if this list has sortable set to true.
+ if ($this->sortable) {
+ $sortColumn = $this->reorderColumn;
+ $this->sortDirection = 'ASC';
+ }
+
$query->orderBy($sortColumn, $this->sortDirection);
}
@@ -782,6 +849,17 @@ protected function defineListColumns()
throw new ApplicationException(Lang::get('backend::lang.list.missing_columns', compact('class')));
}
+ if ($this->sortable) {
+ $this->allColumns['sort_handle'] = $this->makeListColumn('sort_handle', [
+ 'label' => 'backend::lang.list.sort_handle',
+ 'path' => '~/modules/backend/widgets/lists/partials/_list_sort_handle.htm',
+ 'type' => 'partial',
+ 'width' => '20px',
+ 'sortable' => false,
+ 'clickable' => false,
+ ]);
+ }
+
$this->addColumns($this->columns);
/**
@@ -964,6 +1042,11 @@ protected function getTotalColumns()
*/
public function getHeaderValue($column)
{
+ // The sort handle should never have a visible column header label.
+ if ($column->label === 'backend::lang.list.sort_handle') {
+ return '';
+ }
+
$value = Lang::get($column->label);
/**
diff --git a/modules/backend/widgets/lists/assets/js/winter.list.js b/modules/backend/widgets/lists/assets/js/winter.list.js
index c2bb07baa0..fcd199d4a2 100644
--- a/modules/backend/widgets/lists/assets/js/winter.list.js
+++ b/modules/backend/widgets/lists/assets/js/winter.list.js
@@ -11,6 +11,7 @@
var $el = this.$el = $(element);
this.options = options || {};
+ this.sortOrders = []
var scrollClassContainer = options.scrollClassContainer !== undefined
? options.scrollClassContainer
@@ -22,6 +23,17 @@
dragSelector: 'thead'
})
+ if (element.dataset.hasOwnProperty('sortable')) {
+ this.$el.find('.control-list-tbody').listSortable({
+ handle: '.drag-handle'
+ })
+ this.$el.on('dragged.list.sorted', $.proxy(this.processReorder, this))
+
+ this.$el.find('[data-record-sort-order]').each(function (index, el) {
+ this.sortOrders.push(el.dataset.recordSortOrder)
+ }.bind(this))
+ }
+
this.update()
}
@@ -86,6 +98,21 @@
$checkbox.prop('checked', !$checkbox.is(':checked')).trigger('change')
}
+ ListWidget.prototype.processReorder = function() {
+ var relation = this.$el.data('sortableRelation')
+ var handler = relation ? 'onReorderRelation' : 'onReorder'
+
+ var recordIds = []
+ this.$el.find('[data-record-id]').each(function (index, el) {
+ recordIds.push(el.dataset.recordId)
+ }.bind(this))
+
+ this.$el.request(handler, {
+ data: { sort_orders: this.sortOrders, record_ids: recordIds, _reorder_relation_name: relation },
+ loading: $.wn.stripeLoadIndicator,
+ })
+ }
+
// LIST WIDGET PLUGIN DEFINITION
// ============================
@@ -143,4 +170,4 @@
$('[data-control="listwidget"]').listWidget();
})
-}(window.jQuery);
\ No newline at end of file
+}(window.jQuery);
diff --git a/modules/backend/widgets/lists/partials/_list.php b/modules/backend/widgets/lists/partials/_list.php
index 5b3e50e656..5347b00fa5 100644
--- a/modules/backend/widgets/lists/partials/_list.php
+++ b/modules/backend/widgets/lists/partials/_list.php
@@ -1,9 +1,18 @@
-