Skip to content

Commit 2210f7f

Browse files
authored
feat: add sortUsing function to column for custom sorting (#2066)
* feat: add sortUsing function to column for custom sorting * fix: CustomSortTest
1 parent adf7a8c commit 2210f7f

7 files changed

Lines changed: 400 additions & 12 deletions

File tree

src/Column.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PowerComponents\LivewirePowerGrid;
44

5+
use Closure;
56
use Illuminate\Support\Traits\Macroable;
67
use Livewire\Wireable;
78

@@ -41,6 +42,8 @@ final class Column implements Wireable
4142

4243
public bool $sortable = false;
4344

45+
public ?Closure $sortCallback = null;
46+
4447
public bool $index = false;
4548

4649
public array $properties = [];
@@ -175,6 +178,21 @@ public function sortable(): Column
175178
return $this;
176179
}
177180

181+
/**
182+
* Sets a custom sorting callback for this column.
183+
* The callback receives the query builder and sort direction.
184+
*/
185+
public function sortUsing(Closure $callback): Column
186+
{
187+
$this->enableSort();
188+
189+
$this->sortable = true;
190+
191+
$this->sortCallback = $callback;
192+
193+
return $this;
194+
}
195+
178196
/**
179197
* Field in the database
180198
*/
@@ -289,7 +307,12 @@ public function template(): Column
289307

290308
public function toLivewire(): array
291309
{
292-
return (array) $this;
310+
$data = (array) $this;
311+
312+
// Closures cannot be serialized, exclude them
313+
unset($data['sortCallback']);
314+
315+
return $data;
293316
}
294317

295318
public static function fromLivewire($value)

src/Concerns/Sorting.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,23 @@ public function updatedSortField(): void
115115
data_set($this->setUp, 'lazy.items', 0);
116116
}
117117
}
118+
119+
/**
120+
* Get the sort callback for a given field from the columns definition.
121+
* Returns null if no custom callback is defined.
122+
*/
123+
public function getSortCallback(string $field): ?\Closure
124+
{
125+
$columns = $this->columns();
126+
127+
foreach ($columns as $column) {
128+
$columnDataField = data_get($column, 'dataField');
129+
130+
if ($columnDataField === $field && data_get($column, 'sortCallback') instanceof \Closure) {
131+
return $column->sortCallback;
132+
}
133+
}
134+
135+
return null;
136+
}
118137
}

src/DataSource/Processors/Collection/Pipelines/Sorting.php

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,52 @@ public function handle(Collection $collection, Closure $next): Collection
1717
}
1818

1919
if ($this->component->multiSort) {
20-
$sortArray = [];
20+
return $next($this->applyMultipleSort($collection));
21+
}
22+
23+
return $next($this->applySingleSort($collection, $this->component->sortField, $this->component->sortDirection));
24+
}
25+
26+
private function applySingleSort(Collection $collection, string $sortField, string $direction): Collection
27+
{
28+
$sortCallback = $this->component->getSortCallback($sortField);
29+
30+
if ($sortCallback !== null) {
31+
return $sortCallback($collection, $direction);
32+
}
33+
34+
$isDescending = $direction === 'desc';
2135

22-
foreach ($this->component->sortArray as $sortField => $sortDirection) {
23-
$sortArray[] = [$sortField, $sortDirection];
36+
return $collection->sortBy($sortField, SORT_REGULAR, $isDescending);
37+
}
38+
39+
private function applyMultipleSort(Collection $collection): Collection
40+
{
41+
$sortArray = [];
42+
$callbackFields = [];
43+
44+
foreach ($this->component->sortArray as $sortField => $sortDirection) {
45+
$sortCallback = $this->component->getSortCallback($sortField);
46+
47+
if ($sortCallback !== null) {
48+
$callbackFields[] = ['field' => $sortField, 'direction' => $sortDirection, 'callback' => $sortCallback];
49+
50+
continue;
2451
}
2552

26-
return $next($collection->sortBy($sortArray));
53+
$sortArray[] = [$sortField, $sortDirection];
2754
}
2855

29-
$isDescending = $this->component->sortDirection === 'desc';
56+
// Apply standard sorting first
57+
if (filled($sortArray)) {
58+
$collection = $collection->sortBy($sortArray);
59+
}
3060

31-
$sorted = $collection->sortBy($this->component->sortField, SORT_REGULAR, $isDescending);
61+
// Apply callback sorting
62+
foreach ($callbackFields as $callbackField) {
63+
$collection = $callbackField['callback']($collection, $callbackField['direction']);
64+
}
3265

33-
return $next($sorted);
66+
return $collection;
3467
}
3568
}

src/DataSource/Processors/Database/Pipelines/Sorting.php

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,37 @@ public function handle(mixed $query, Closure $next): mixed
2323
if ($this->component->multiSort) {
2424
$this->applyMultipleSort($query);
2525
} else {
26-
$query->orderBy(
27-
$this->makeSortField($this->component->sortField),
28-
$this->component->sortDirection
29-
);
26+
$this->applySingleSort($query, $this->component->sortField, $this->component->sortDirection);
3027
}
3128
}
3229

3330
return $next($query);
3431
}
3532

33+
private function applySingleSort(EloquentBuilder|MorphToMany|QueryBuilder $query, string $sortField, string $direction): void
34+
{
35+
$sortCallback = $this->component->getSortCallback($sortField);
36+
37+
if ($sortCallback !== null) {
38+
$sortCallback($query, $direction);
39+
40+
return;
41+
}
42+
43+
$query->orderBy($this->makeSortField($sortField), $direction);
44+
}
45+
3646
private function applyMultipleSort(EloquentBuilder|MorphToMany|QueryBuilder $results): void
3747
{
3848
foreach ($this->component->sortArray as $sortField => $direction) {
49+
$sortCallback = $this->component->getSortCallback($sortField);
50+
51+
if ($sortCallback !== null) {
52+
$sortCallback($results, $direction);
53+
54+
continue;
55+
}
56+
3957
$results->orderBy($this->makeSortField($sortField), $direction);
4058
}
4159
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
namespace PowerComponents\LivewirePowerGrid\Tests\Concerns\Components;
4+
5+
use Illuminate\Support\Collection;
6+
use PowerComponents\LivewirePowerGrid\{Column, Facades\PowerGrid, PowerGridComponent, PowerGridFields};
7+
8+
class DishesCustomSortCollectionTable extends PowerGridComponent
9+
{
10+
public string $tableName = 'dishes-custom-sort-collection-table';
11+
12+
public function datasource(): Collection
13+
{
14+
return collect([
15+
['id' => 1, 'name' => 'Zebra Dish', 'price' => 100.00, 'calories' => 300],
16+
['id' => 2, 'name' => 'Apple Pie', 'price' => 50.00, 'calories' => 200],
17+
['id' => 3, 'name' => 'Banana Split', 'price' => 75.00, 'calories' => 400],
18+
['id' => 4, 'name' => 'Cherry Tart', 'price' => 25.00, 'calories' => 100],
19+
['id' => 5, 'name' => 'Donut', 'price' => 10.00, 'calories' => 500],
20+
]);
21+
}
22+
23+
public function setUp(): array
24+
{
25+
return [
26+
PowerGrid::header()
27+
->showSearchInput(),
28+
29+
PowerGrid::footer()
30+
->showPerPage()
31+
->showRecordCount(),
32+
];
33+
}
34+
35+
public function fields(): PowerGridFields
36+
{
37+
return PowerGrid::fields()
38+
->add('id')
39+
->add('name')
40+
->add('price')
41+
->add('calories');
42+
}
43+
44+
public function columns(): array
45+
{
46+
return [
47+
Column::make('Id', 'id')
48+
->sortable(),
49+
50+
Column::make('Name', 'name')
51+
->sortable(),
52+
53+
Column::make('Price', 'price')
54+
->sortUsing(function (Collection $collection, string $direction) {
55+
// Custom sort: sort by price with rounding to nearest 50
56+
return $collection->sortBy(function ($item) {
57+
return round(data_get($item, 'price') / 50) * 50;
58+
}, SORT_REGULAR, $direction === 'desc');
59+
}),
60+
61+
Column::make('Calories', 'calories')
62+
->sortUsing(function (Collection $collection, string $direction) {
63+
// Custom sort: sort by calories in groups (low/medium/high)
64+
return $collection->sortBy(function ($item) {
65+
$calories = data_get($item, 'calories');
66+
if ($calories <= 200) {
67+
return 1; // Low
68+
}
69+
if ($calories <= 400) {
70+
return 2; // Medium
71+
}
72+
73+
return 3; // High
74+
}, SORT_REGULAR, $direction === 'desc');
75+
}),
76+
77+
Column::action('Action'),
78+
];
79+
}
80+
81+
public function setTestThemeClass(string $themeClass): void
82+
{
83+
config(['livewire-powergrid.theme' => $themeClass]);
84+
}
85+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
namespace PowerComponents\LivewirePowerGrid\Tests\Concerns\Components;
4+
5+
use Illuminate\Database\Eloquent\Builder;
6+
use PowerComponents\LivewirePowerGrid\{Column, Facades\PowerGrid, PowerGridComponent, PowerGridFields};
7+
use PowerComponents\LivewirePowerGrid\Tests\Concerns\Models\Dish;
8+
9+
class DishesCustomSortTable extends PowerGridComponent
10+
{
11+
public string $tableName = 'dishes-custom-sort-table';
12+
13+
public function setUp(): array
14+
{
15+
return [
16+
PowerGrid::header()
17+
->showSearchInput(),
18+
19+
PowerGrid::footer()
20+
->showPerPage()
21+
->showRecordCount(),
22+
];
23+
}
24+
25+
public function datasource(): Builder
26+
{
27+
return Dish::query();
28+
}
29+
30+
public function fields(): PowerGridFields
31+
{
32+
return PowerGrid::fields()
33+
->add('id')
34+
->add('name')
35+
->add('price')
36+
->add('calories');
37+
}
38+
39+
public function columns(): array
40+
{
41+
return [
42+
Column::make('Id', 'id')
43+
->sortable(),
44+
45+
Column::make('Name', 'name')
46+
->sortable(),
47+
48+
Column::make('Price', 'price')
49+
->sortUsing(function ($query, string $direction) {
50+
// Custom sort: sort by price, but nulls go last
51+
$query->orderByRaw("price IS NULL, price {$direction}");
52+
}),
53+
54+
Column::make('Calories', 'calories')
55+
->sortUsing(function ($query, string $direction) {
56+
// Custom sort: multiply calories by a factor and sort
57+
$query->orderByRaw("calories * 2 {$direction}");
58+
}),
59+
60+
Column::action('Action'),
61+
];
62+
}
63+
64+
public function setTestThemeClass(string $themeClass): void
65+
{
66+
config(['livewire-powergrid.theme' => $themeClass]);
67+
}
68+
}

0 commit comments

Comments
 (0)