From f81ca37b3b29fc9ef681cefd4e2439955f8f0fff Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Thu, 18 Jun 2026 22:37:08 -0600 Subject: [PATCH 1/2] Document inline drag-and-drop sorting for lists and relations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents the inline reordering feature added in wintercms/winter#1491 (issue wintercms/winter#1472): - database/traits.md: complete the HasSortableRelations section (it was an unclosed, incomplete stub) — usage, pivot setup, automatic ordering, setRelationOrder() and isSortableRelation() - backend/lists.md: add the `sortable` list option and a "Reordering records" section - backend/relations.md: add the view-only `sortable` option and a "Reordering relations" section (incl. deferred/unsaved parent support) - backend/reorder.md: note the inline alternatives to the standalone Reorder page Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/lists.md | 13 +++++++++++++ backend/relations.md | 16 ++++++++++++++++ backend/reorder.md | 2 ++ database/traits.md | 43 ++++++++++++++++++++++++++++++++++++++----- 4 files changed, 69 insertions(+), 5 deletions(-) diff --git a/backend/lists.md b/backend/lists.md index 3e6786ae..be373912 100644 --- a/backend/lists.md +++ b/backend/lists.md @@ -76,6 +76,19 @@ Option | Description `showTotals` | displays the summed values for the columns in the form of `totalOnPage (totalForQuery)` in the list header and footer. Default: `true`. `treeExpanded` | if tree nodes should be expanded by default. Default: `false`. `customViewPath` | specify a custom view path to override partials used by the list, optional. +`sortable` | enables drag-and-drop reordering of records directly in the list, see [reordering records](#reordering-records). Default: `false`. + +### Reordering records + +Set `sortable` to `true` to let backend users reorder the list with drag-and-drop. The list model must use the [`Sortable` trait](../database/traits#sortable) so it has a `sort_order` column. + +```yaml +sortable: true +``` + +When enabled, a drag handle is shown on each row, column header sorting and pagination are disabled (every record is shown in its stored order), and dropping a row persists the new order to the model's sort order column via AJAX. + +> **NOTE:** Reordering applies to flat lists. For reordering tree structures, or for a dedicated standalone reordering page, use the [Reorder behavior](reorder). ### Adding a toolbar diff --git a/backend/relations.md b/backend/relations.md index e109d20e..0d5788cd 100644 --- a/backend/relations.md +++ b/backend/relations.md @@ -114,6 +114,7 @@ Option | Type | Description `recordUrl` | List | link each list record to another page. Eg: **users/update/:id**. The `:id` part is replaced with the record identifier. `customViewPath` | List | specify a custom view path to override partials used by the list. `recordOnClick` | List | custom JavaScript code to execute when clicking on a record. +`sortable` | List | enables drag-and-drop reordering of the related records, see [reordering relations](#reordering-relations). Requires the parent model to use the [`HasSortableRelations` trait](../database/traits#hassortablerelations). Default: `false`. `toolbarPartial` | Both | a reference to a controller partial file with the toolbar buttons. Eg: **_relation_toolbar.htm**. This option overrides the *toolbarButtons* option. `toolbarButtons` | Both | the set of buttons to display. This can be formatted as an array or a pipe separated string, or set to `false` to show no buttons. Available options are: `create`, `update`, `delete`, `add`, `remove`, `refresh`, `link`, & `unlink`. Example: `add\|remove`.
Additionally, you can customize the text inside these buttons by setting this property to an associative array, with the key being the button type and the value being the text for that button. Example: `create: 'Assign User'`. The value also supports translation. @@ -289,6 +290,21 @@ phone: list: $/acme/user/models/phone/columns.yaml ``` +### Reordering relations + +Pivot-based relations (`belongsToMany`, `morphToMany`, `morphedByMany`) can be reordered with drag-and-drop directly in the relation manager. The parent model must use the [`HasSortableRelations` trait](../database/traits#hassortablerelations) and declare the relation in its `$sortableRelations` property, and the pivot table must have a sort order column. Then set `sortable: true` on the relation's `view` configuration: + +```yaml +authors: + label: Author + view: + list: $/acme/blog/models/author/columns.yaml + toolbarButtons: link|unlink + sortable: true +``` + +A drag handle is shown on each related record; dropping persists the new order to the pivot's sort order column. Reordering also works while the parent record is being created, before it is saved — the order is stored against the [deferred binding](../database/relations#deferred-binding) and committed together with the record. + ## Displaying a relation manager Before relations can be managed on any page, the target model must first be initialized in the controller by calling the `initRelation` method. diff --git a/backend/reorder.md b/backend/reorder.md index 7921f099..0348a3a0 100644 --- a/backend/reorder.md +++ b/backend/reorder.md @@ -4,6 +4,8 @@ The **Reorder behavior** is a controller [behavior](../services/behaviors) that provides features for sorting and reordering database records. The behavior provides a page called Reorder using the controller action `reorder`. This page displays a list of records with a drag handle allowing them to be sorted and in some cases restructured. +> **NOTE:** To let users reorder records inline with drag-and-drop without a dedicated page — directly in a [list](lists#reordering-records) or a [relation manager](relations#reordering-relations) — see those sections. The Reorder behavior documented here provides a dedicated standalone page, which is best suited to models with deep tree structures. + The behavior depends on a [model class](../database/model) which must implement one of the following [model traits](../database/traits): 1. [`Winter\Storm\Database\Traits\Sortable`](../database/traits#sortable) diff --git a/database/traits.md b/database/traits.md index 6cf583f9..22433364 100644 --- a/database/traits.md +++ b/database/traits.md @@ -838,16 +838,49 @@ class ApiData extends Model ## HasSortableRelations -Add this trait to your model in order to allow its relations to be sorted/reordered. +Sorted relations store a sort order value in the pivot table of a `belongsToMany`, `morphToMany`, or `morphedByMany` relation, so the related records keep a custom order for each parent record. Apply the `Winter\Storm\Database\Traits\HasSortableRelations` trait and define a `$sortableRelations` property that maps each relation name to its pivot sort order column. ```php -class MyModel extends model +class Article extends \Winter\Storm\Database\Model { use \Winter\Storm\Database\Traits\HasSortableRelations; /** - * @var array Relations that can be sorted/reordered and the column name to use for sorting/reordering. + * @var array Relations that can be reordered and the pivot column used for sorting. */ - public $sortableRelations = ['relation_name' => 'sort_order_column']; -... + public $sortableRelations = ['authors' => 'sort_order']; + + public $belongsToMany = [ + 'authors' => [ + \Acme\Blog\Models\Author::class, + 'table' => 'acme_blog_articles_authors', + ], + ]; } +``` + +Ensure the pivot table has the sort order column, for example in a migration: + +```php +$table->integer('sort_order')->default(0); +``` + +When the trait boots it automatically adds the sort order column to the relation's pivot data and applies an `order by {pivot_table}.{column} asc` clause, so the relation is always returned in its stored order. Newly attached records are appended to the end of the relation automatically. + +Use the `setRelationOrder` method to reorder a relation programmatically. Pass the related record ids in their new order; an optional second argument provides the sort order value to assign to each (when omitted, a sequential `1..N` order is assigned in the given order): + +```php +// Assign sort orders 1, 2, 3 to the given records, in this order +$article->setRelationOrder('authors', [$author3->id, $author1->id, $author2->id]); + +// Assign explicit sort order values +$article->setRelationOrder('authors', [1, 2, 3], [3, 2, 1]); +``` + +You can check whether a relation is configured as sortable with `isSortableRelation`: + +```php +$article->isSortableRelation('authors'); // true +``` + +> **NOTE:** To let backend users reorder a relation with drag-and-drop directly in a form, see [reordering relations](../backend/relations#reordering-relations) in the RelationController documentation. From 32923e3288fca99e18e3a14aa22ff70e9964f6ac Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Thu, 18 Jun 2026 23:54:38 -0600 Subject: [PATCH 2/2] Clarify HasSortableRelations docs per review feedback - Make clear the sort order column must be created by the user (migration); the trait adds it to the relation's pivot data and applies the order clause, it does not create the database column. - Fix the setRelationOrder parameter description ("third argument", not "second") and use distinct record ids in the explicit-orders example so the ids and sort order values aren't visually conflated. Co-Authored-By: Claude Opus 4.8 (1M context) --- database/traits.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/database/traits.md b/database/traits.md index 22433364..c67888b5 100644 --- a/database/traits.md +++ b/database/traits.md @@ -865,16 +865,16 @@ Ensure the pivot table has the sort order column, for example in a migration: $table->integer('sort_order')->default(0); ``` -When the trait boots it automatically adds the sort order column to the relation's pivot data and applies an `order by {pivot_table}.{column} asc` clause, so the relation is always returned in its stored order. Newly attached records are appended to the end of the relation automatically. +You must create the column yourself (as in the migration above) — the trait does not create it. When the model boots, the trait includes that column in the relation's pivot data, so its value is loaded onto each record's `pivot`, and applies an `order by {pivot_table}.{column} asc` clause so the relation is always returned in its stored order. The trait also assigns the next sort order value to newly attached records, appending them to the end of the relation. -Use the `setRelationOrder` method to reorder a relation programmatically. Pass the related record ids in their new order; an optional second argument provides the sort order value to assign to each (when omitted, a sequential `1..N` order is assigned in the given order): +Use the `setRelationOrder` method to reorder a relation programmatically. The second argument is the related record ids in their new order; the optional third argument provides the sort order value to assign to each (when omitted, a sequential `1..N` order is assigned in the given order): ```php -// Assign sort orders 1, 2, 3 to the given records, in this order +// Reorder by ids only — assigns sort orders 1, 2, 3 in the given order $article->setRelationOrder('authors', [$author3->id, $author1->id, $author2->id]); -// Assign explicit sort order values -$article->setRelationOrder('authors', [1, 2, 3], [3, 2, 1]); +// Reorder by ids (second argument) with explicit sort order values (third argument) +$article->setRelationOrder('authors', [$author1->id, $author2->id, $author3->id], [3, 2, 1]); ``` You can check whether a relation is configured as sortable with `isSortableRelation`: