Skip to content

Commit 32b86a1

Browse files
authored
Add reusable data table component (#45)
1 parent 03897f4 commit 32b86a1

File tree

69 files changed

+3407
-143
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+3407
-143
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Http\Controllers;
6+
7+
use App\Http\Resources\UserResource;
8+
use App\Models\User;
9+
use App\Support\SpatieQueryBuilder\Filters\DateFilter;
10+
use Illuminate\Database\Eloquent\Builder;
11+
use Illuminate\Http\Request;
12+
use Inertia\Inertia;
13+
use Inertia\Response;
14+
use Spatie\QueryBuilder\AllowedFilter;
15+
use Spatie\QueryBuilder\Enums\FilterOperator;
16+
use Spatie\QueryBuilder\QueryBuilder;
17+
18+
final class DashboardController extends Controller
19+
{
20+
public function index(Request $request): Response
21+
{
22+
$users = QueryBuilder::for(User::class)
23+
->defaultSort('name')
24+
->allowedSorts(['name', 'email', 'language'])
25+
->allowedFilters([
26+
AllowedFilter::callback('search', function (Builder $query, string $value): void {
27+
$query->where(function (Builder $query) use ($value): void {
28+
$query->whereLike(column: 'name', value: "%{$value}%")
29+
->orWhereLike(column: 'email', value: "%{$value}%");
30+
});
31+
}),
32+
AllowedFilter::partial('_name', 'name'),
33+
AllowedFilter::operator('name', FilterOperator::DYNAMIC),
34+
AllowedFilter::partial('_email', 'email'),
35+
AllowedFilter::operator('email', FilterOperator::DYNAMIC),
36+
AllowedFilter::operator('language', FilterOperator::DYNAMIC),
37+
AllowedFilter::callback('verified', function (Builder $query, bool $value): void {
38+
if ($value) {
39+
$query->whereNotNull('email_verified_at');
40+
} else {
41+
$query->whereNull('email_verified_at');
42+
}
43+
}),
44+
AllowedFilter::custom('created_at', new DateFilter),
45+
])
46+
->paginate()
47+
->withQueryString();
48+
49+
return Inertia::render('dashboard/index', [
50+
'users' => UserResource::collection($users),
51+
'sorts' => $request->getSorts(),
52+
'filters' => $request->getFilters(),
53+
]);
54+
}
55+
}

app/Mixins/RequestMixin.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Mixins;
6+
7+
use Closure;
8+
use Illuminate\Http\Request;
9+
10+
/**
11+
* @mixin Request
12+
*/
13+
final class RequestMixin
14+
{
15+
public function getSorts(): Closure
16+
{
17+
/**
18+
* @return array<int, array{id: string, desc: bool}>
19+
*/
20+
return function (?string $default = null): array {
21+
$sortQuery = $this->query('sort', $default);
22+
23+
if (is_string($sortQuery)) {
24+
$sorts = explode(',', $sortQuery);
25+
26+
$sorts = array_map(function (string $value): array {
27+
$value = trim($value);
28+
$desc = str_starts_with($value, '-');
29+
$id = ltrim($value, '-');
30+
31+
return [
32+
'id' => $id,
33+
'desc' => $desc,
34+
];
35+
}, $sorts);
36+
37+
return array_filter($sorts, fn (array $sort): bool => $sort['id'] !== '');
38+
}
39+
40+
return [];
41+
};
42+
}
43+
44+
public function getFilters(): Closure
45+
{
46+
/**
47+
* @return array<int, array{id: string, value: string}>
48+
*/
49+
return function (?array $default = null): array {
50+
$filters = $this->query('filter', $default);
51+
52+
if (is_array($filters)) {
53+
$filters = array_map(fn (mixed $value, string $key): array => [
54+
'id' => $key,
55+
'value' => is_array($value) ? implode(',', $value) : $value,
56+
], $filters, array_keys($filters));
57+
58+
return array_filter($filters, fn (array $filter): bool => $filter['id'] !== '');
59+
}
60+
61+
return [];
62+
};
63+
}
64+
}

app/Providers/AppServiceProvider.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@
44

55
namespace App\Providers;
66

7+
use App\Mixins\RequestMixin;
78
use Carbon\CarbonImmutable;
89
use Illuminate\Database\Eloquent\Model;
10+
use Illuminate\Http\Request;
11+
use Illuminate\Pagination\LengthAwarePaginator;
912
use Illuminate\Support\Facades\Date;
1013
use Illuminate\Support\Facades\DB;
1114
use Illuminate\Support\Facades\Vite;
1215
use Illuminate\Support\ServiceProvider;
1316
use Override;
17+
use ReflectionException;
1418

1519
final class AppServiceProvider extends ServiceProvider
1620
{
@@ -27,12 +31,17 @@ public function register(): void
2731
// @codeCoverageIgnoreEnd
2832
}
2933

34+
/**
35+
* @throws ReflectionException
36+
*/
3037
public function boot(): void
3138
{
3239
$this->configureCommands();
3340
$this->configureModels();
3441
$this->configureVite();
3542
$this->configureDates();
43+
$this->configureMixins();
44+
$this->configurePagination();
3645
}
3746

3847
private function configureCommands(): void
@@ -56,4 +65,22 @@ private function configureDates(): void
5665
{
5766
Date::use(CarbonImmutable::class);
5867
}
68+
69+
/**
70+
* @throws ReflectionException
71+
*/
72+
private function configureMixins(): void
73+
{
74+
Request::mixin(new RequestMixin);
75+
}
76+
77+
private function configurePagination(): void
78+
{
79+
$this->app->extend(LengthAwarePaginator::class, function (LengthAwarePaginator $paginator): LengthAwarePaginator {
80+
// Ensures there are not too many links in the pagination
81+
$paginator->onEachSide(1);
82+
83+
return $paginator;
84+
});
85+
}
5986
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Support\SpatieQueryBuilder\Filters;
6+
7+
use Carbon\CarbonImmutable;
8+
use Carbon\CarbonPeriodImmutable;
9+
use Carbon\Exceptions\InvalidFormatException;
10+
use Illuminate\Database\Eloquent\Builder;
11+
use Illuminate\Database\Eloquent\Model;
12+
use InvalidArgumentException;
13+
use Spatie\QueryBuilder\Enums\FilterOperator;
14+
use Spatie\QueryBuilder\Filters\Filter;
15+
16+
/**
17+
* @implements Filter<Model>
18+
*/
19+
final class DateFilter implements Filter
20+
{
21+
public function __invoke(Builder $query, mixed $value, string $property): void
22+
{
23+
if (is_array($value)) {
24+
if (count($value) < 2) {
25+
return;
26+
}
27+
28+
try {
29+
$period = new CarbonPeriodImmutable($value[0], $value[1]);
30+
31+
$query->whereBetween($property, $period->setTimezone('UTC'));
32+
33+
return;
34+
} catch (InvalidArgumentException) {
35+
return;
36+
}
37+
}
38+
39+
$value = (string) $value;
40+
41+
$filterOperator = $this->getFilterOperator($value);
42+
43+
$this->removeFilterOperatorFromValue($value, $filterOperator);
44+
45+
if ($value === '') {
46+
return;
47+
}
48+
49+
try {
50+
$date = new CarbonImmutable($value);
51+
52+
match ($filterOperator) {
53+
FilterOperator::EQUAL => $query->whereDate($property, $date),
54+
FilterOperator::NOT_EQUAL => $query->whereDate($property, '!=', $date),
55+
FilterOperator::GREATER_THAN => $query->whereDate($property, '>', $date),
56+
FilterOperator::GREATER_THAN_OR_EQUAL => $query->whereDate($property, '>=', $date),
57+
FilterOperator::LESS_THAN => $query->whereDate($property, '<', $date),
58+
FilterOperator::LESS_THAN_OR_EQUAL => $query->whereDate($property, '<=', $date),
59+
default => null
60+
};
61+
} catch (InvalidFormatException) {
62+
return;
63+
}
64+
}
65+
66+
private function getFilterOperator(string $value): FilterOperator
67+
{
68+
$filterOperator = FilterOperator::EQUAL;
69+
70+
foreach (FilterOperator::cases() as $filterOperatorCase) {
71+
if (str_starts_with($value, $filterOperatorCase->value) && ! $filterOperatorCase->isDynamic()) {
72+
$filterOperator = $filterOperatorCase;
73+
}
74+
}
75+
76+
return $filterOperator;
77+
}
78+
79+
private function removeFilterOperatorFromValue(string &$value, FilterOperator $filterOperator): void
80+
{
81+
if (str_starts_with($value, $filterOperator->value)) {
82+
$value = substr($value, strlen($filterOperator->value));
83+
}
84+
}
85+
}

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"laravel/tinker": "^v2.9",
1717
"league/flysystem-aws-s3-v3": "^3.24",
1818
"pragmarx/google2fa": "^v8.0",
19+
"spatie/laravel-query-builder": "^6.3",
1920
"tightenco/ziggy": "^v2.0"
2021
},
2122
"require-dev": {

0 commit comments

Comments
 (0)