Skip to content

Commit f333c6d

Browse files
authored
Merge pull request #5 from flavorly/feat-rich-select-fetch
Feat rich select fetch
2 parents 107c758 + 554464a commit f333c6d

File tree

13 files changed

+352
-60
lines changed

13 files changed

+352
-60
lines changed

CHANGELOG.md

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,4 @@
22

33
All notable changes to `laravel-vanilla-components` will be documented in this file.
44

5-
## Datatables : Query & Response - 2023-02-14
6-
7-
Datatables : Refactor to response() method
8-
Datatables: Ability to override filters, sorting & search methods
9-
Datatables: Removed Json Resource in favor of a simple & easy approach
10-
11-
## Datatables Bugfixes - 2023-01-26
12-
13-
Datatables : Fixed issue with Option for showing number of items not being passed
14-
Datatables: Fixed Paginator URL window for a better slider on smaller amount of pages
15-
16-
## Datatables : Closures for Query & Data Transform - 2022-12-18
17-
18-
Datatables : Ability to Have closures Resolving Query, Fields on Transform and Also
19-
Datatables : Abitlity to have HasMany relations & Fixed issues with Relationships Loading
20-
21-
## v0.0.3 - 2022-11-25
22-
23-
Datatables: Fixed various bugs with Fetching Records/Models on Actions
24-
Datatables: Ability to Refresh / not after a action is dispatched.
25-
Components: Fixed issue with Select Options
5+
Changelog will be updated on: [Online Changelog](https://vanilla-components.com/guide/changelog.html).
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace Flavorly\VanillaComponents\Core\Components\Concerns;
4+
5+
use Closure;
6+
7+
trait HasFetchOptions
8+
{
9+
protected string|Closure|null $fetchOptionsEndpoint = null;
10+
11+
protected string|Closure|null $fetchOptionKey = null;
12+
13+
protected string|Closure|null $fetchOptionLabel = null;
14+
15+
public function fetchOptionsFrom(string|Closure|null $url, string|Closure $label, string|Closure|null $key = 'id'): static
16+
{
17+
$this->fetchOptionsEndpoint = $url;
18+
$this->fetchOptionKey = $key;
19+
$this->fetchOptionLabel = $label;
20+
21+
return $this;
22+
}
23+
24+
public function getFetchOptionKey(): ?string
25+
{
26+
return $this->evaluate($this->fetchOptionKey);
27+
}
28+
29+
public function getFetchOptionLabel(): ?string
30+
{
31+
return $this->evaluate($this->fetchOptionLabel);
32+
}
33+
34+
public function getFetchOptionsEndpoint(): ?string
35+
{
36+
return $this->evaluate($this->fetchOptionsEndpoint);
37+
}
38+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Flavorly\VanillaComponents\Core\Components\Concerns;
4+
5+
use Closure;
6+
7+
trait HasMultipleValues
8+
{
9+
protected bool|Closure $isMultiple = false;
10+
11+
public function multiple(Closure|bool $condition = true): static
12+
{
13+
$this->isMultiple = $condition;
14+
15+
return $this;
16+
}
17+
18+
public function getIsMultiple(): bool
19+
{
20+
return $this->evaluate($this->isMultiple);
21+
}
22+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
namespace Flavorly\VanillaComponents\Core\Components\Helpers;
4+
5+
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
6+
use Illuminate\Database\Eloquent\Builder;
7+
use Illuminate\Database\Eloquent\Model;
8+
use Illuminate\Support\Str;
9+
use Laravel\Scout\Searchable;
10+
11+
class ResolveRichSelectOptions
12+
{
13+
public function __construct(
14+
protected string $model,
15+
protected array $columns = [],
16+
protected bool $useScout = true,
17+
protected int $perPage = 10,
18+
) {
19+
}
20+
21+
/**
22+
* Builds the query and returns the paginator
23+
*/
24+
public function response(): LengthAwarePaginator
25+
{
26+
// Store the values
27+
$searchQuery = request()->query('query');
28+
$values = request()->query('values');
29+
$page = request()->query('page', 1);
30+
31+
/** @var Model|Builder $model */
32+
$model = app($this->model);
33+
$scoutKeys = []; // Stores Laravel scout temporary keys
34+
35+
// If none is filled, return teh latest paginated results
36+
if (! filled($searchQuery) && ! filled($values)) {
37+
return $model::query()->latest()->paginate($this->perPage);
38+
}
39+
40+
if ($this->useScout && in_array(Searchable::class, class_uses($model::class))) {
41+
/** @var Searchable $model */
42+
$scoutKeys = $model::search($searchQuery ?? '')->keys() ?? [];
43+
}
44+
45+
return $model::query()
46+
// If we have values, then we can instantly filter them here
47+
->when(filled($values), function ($query) use ($model, $values) {
48+
$query->whereIn($model->getKeyName(), ! is_array($values) ? Str::of($values)->explode(',') : $values);
49+
})
50+
// Not using scout and using regular search
51+
->when(! $this->useScout && filled($searchQuery) && ! empty($this->columns), function ($query) use ($searchQuery) {
52+
$query->where(function ($query) use ($searchQuery) {
53+
foreach ($this->columns as $column) {
54+
$query->orWhere($column, 'like', "%{$searchQuery}%");
55+
}
56+
});
57+
})
58+
// Using Scout
59+
->when($this->useScout && filled($searchQuery) && ! empty($scoutKeys), function ($query) use ($model, $scoutKeys) {
60+
$query->whereIn($model->getKeyName(), $scoutKeys);
61+
})
62+
// Finally paginate the results
63+
->paginate(perPage: $this->perPage, page: $page);
64+
}
65+
66+
/**
67+
* Resolves the current select options for a given model and given columns
68+
* Supporting scout searching
69+
* This function is intended to be used for frontend pooling with Rich Select
70+
*
71+
* @throws \Exception
72+
*/
73+
public static function for(string $model, array $columns = [], bool $useScout = true): LengthAwarePaginator
74+
{
75+
return (new static($model, $columns, $useScout))->response();
76+
}
77+
}

src/Core/Components/RichSelect.php

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ class RichSelect extends BaseComponent
66
{
77
use Concerns\CanBeSearchable;
88
use Concerns\HasOptions;
9+
use Concerns\HasFetchOptions;
10+
use Concerns\HasMultipleValues;
911

1012
protected string $component = 'VanillaRichSelect';
1113

@@ -20,8 +22,22 @@ public function toArray(): array
2022
'loadingMoreResultsText' => trans('laravel-vanilla-components::translations.select.loadingMoreResultsText'),
2123
]);
2224

23-
return array_merge(parent::toArray(), [
24-
'options' => $this->getOptionsToArray(),
25-
]);
25+
return array_merge(
26+
// Base
27+
parent::toArray(),
28+
[
29+
'multiple' => $this->getIsMultiple(),
30+
],
31+
// Options
32+
[
33+
'options' => $this->getOptionsToArray(),
34+
],
35+
// Fetch Stuff
36+
$this->getFetchOptionsEndpoint() !== null ? [
37+
'fetchEndpoint' => $this->getFetchOptionsEndpoint(),
38+
'valueAttribute' => $this->getFetchOptionKey(),
39+
'textAttribute' => $this->getFetchOptionLabel(),
40+
] : []
41+
);
2642
}
2743
}

src/Datatables/Columns/Column.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class Column implements CoreContracts\HasToArray
1919
use Concerns\HasLabel;
2020
use Concerns\HasName;
2121
use Concerns\HasKey;
22+
use Concerns\InteractsWithTableQuery;
2223
use Macroable;
2324

2425
public function toArray(): array

src/Datatables/Columns/Concerns/CanBeSortable.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,9 @@ trait CanBeSortable
1111

1212
protected ?array $sortColumns = [];
1313

14-
protected ?Closure $sortQuery = null;
15-
1614
protected ?string $sortDirection = null;
1715

18-
public function sortable(bool|array $condition = true, ?Closure $query = null): static
16+
public function sortable(bool|array $condition = true, ?Closure $applySortUsing = null): static
1917
{
2018
if (is_array($condition)) {
2119
$this->isSortable = true;
@@ -25,7 +23,9 @@ public function sortable(bool|array $condition = true, ?Closure $query = null):
2523
$this->sortColumns = null;
2624
}
2725

28-
$this->sortQuery = $query;
26+
if (null !== $applySortUsing) {
27+
$this->sortUsing($applySortUsing);
28+
}
2929

3030
return $this;
3131
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace Flavorly\VanillaComponents\Datatables\Columns\Concerns;
4+
5+
use Closure;
6+
use Illuminate\Database\Eloquent\Builder;
7+
8+
trait InteractsWithTableQuery
9+
{
10+
protected ?Closure $applySortingUsing = null;
11+
12+
public function applySort(Builder $query, string $column, mixed $sorting): Builder
13+
{
14+
if ($sorting === null) {
15+
return $query;
16+
}
17+
18+
if ($this->applySortingUsing instanceof Closure) {
19+
return $this->evaluate($this->applySortingUsing, [
20+
'query' => $query,
21+
'column' => $column,
22+
'sorting' => $sorting,
23+
]);
24+
}
25+
26+
return $query->orderBy($column, $sorting);
27+
}
28+
29+
public function sortUsing(?Closure $callback): static
30+
{
31+
$this->applySortingUsing = $callback;
32+
33+
return $this;
34+
}
35+
}

src/Datatables/Concerns/InteractsWithQueryBuilder.php

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,23 @@
88
use Flavorly\VanillaComponents\Datatables\Filters\Filter;
99
use Flavorly\VanillaComponents\Datatables\Http\Payload\RequestPayload;
1010
use Illuminate\Database\Eloquent\Builder;
11+
use Illuminate\Database\Eloquent\Model;
1112
use Illuminate\Database\Eloquent\Relations\Relation;
1213
use Illuminate\Pagination\LengthAwarePaginator;
1314
use Illuminate\Pagination\UrlWindow;
1415
use Illuminate\Support\Collection;
16+
use Illuminate\Support\Str;
1517
use Laravel\Scout\Builder as ScoutBuilder;
1618
use Laravel\Scout\Searchable;
1719

1820
trait InteractsWithQueryBuilder
1921
{
2022
/**
2123
* Stores the query object
24+
*
25+
* @var Builder|ScoutBuilder|Closure|null|mixed
2226
*/
23-
protected Builder|ScoutBuilder|null|Closure $query = null;
27+
protected mixed $query = null;
2428

2529
/**
2630
* Stores the data comming from the client side
@@ -38,9 +42,9 @@ public function query(): mixed
3842
/**
3943
* Return the query model/instance
4044
*
41-
* @return Closure|Builder|ScoutBuilder|null
45+
* @return Closure|Builder|ScoutBuilder|null|mixed
4246
*/
43-
protected function getQuery()
47+
protected function getQuery(): mixed
4448
{
4549
return $this->query;
4650
}
@@ -97,14 +101,18 @@ protected function resolveQueryOrModel(mixed $queryOrModel = null): Builder
97101
/**
98102
* Apply the user provided filters using the follow method
99103
*/
100-
protected function applyQueryFilters(Builder $query, RequestPayload $payload): Builder
104+
protected function applyQueryFilters(Builder $query, RequestPayload $payload): void
101105
{
102-
return $query->when($payload->hasFilters(), function (Builder $subQuery) use ($payload) {
106+
$query->when($payload->hasFilters(), function (Builder $subQuery) use ($payload) {
103107
// Each column that needs to be sorted
104108
$payload
105109
->getFilters()
106110
// Apply Sorting
107-
->each(fn (Filter $filter) => $filter->apply($subQuery, $filter->getName(), $filter->getValue()));
111+
->each(fn (Filter $filter) => $filter->apply(
112+
$subQuery,
113+
$filter->getName(),
114+
$filter->getValue()
115+
));
108116

109117
return $subQuery;
110118
});
@@ -113,13 +121,13 @@ protected function applyQueryFilters(Builder $query, RequestPayload $payload): B
113121
/**
114122
* Apply the sorting using the following method
115123
*/
116-
protected function applyQuerySorting(Builder $query, RequestPayload $payload): Builder
124+
protected function applyQuerySorting(Builder $query, RequestPayload $payload): void
117125
{
118-
return $query->when($payload->hasSorting(), function (Builder $subQuery) use ($payload) {
126+
$query->when($payload->hasSorting(), function (Builder $subQuery) use ($payload) {
119127
// Each column that needs to be sorted
120128
$payload
121129
->getSorting()
122-
->each(fn (Column $column) => $subQuery->orderBy($column->getName(), $column->getSortDirection()));
130+
->each(fn (Column $column) => $column->applySort($subQuery, $column->getName(), $column->getSortDirection()));
123131

124132
return $subQuery;
125133
});
@@ -128,17 +136,18 @@ protected function applyQuerySorting(Builder $query, RequestPayload $payload): B
128136
/**
129137
* Apply the search using the following method, supporting scout if the class uses scout.
130138
*/
131-
protected function applySearch(Builder $query, RequestPayload $payload): Builder
139+
protected function applySearch(Builder $query, RequestPayload $payload): void
132140
{
141+
/** @var Model $model */
133142
$model = $this->getQuery()->getModel();
134143
$usingScout = in_array(Searchable::class, class_uses($model::class));
135144

136-
return $query
145+
$query
137146
// Model is using Scout, we can use it.
138147
->when($usingScout && $payload->hasSearch(), function (Builder $subQuery) use ($model) {
139148
// Each column that needs to be sorted
140-
/** @var Searchable $model */
141-
$subQuery->whereIn('id', $model::search($this->data->getSearch())->keys());
149+
/** @var Searchable|Model $model */
150+
$subQuery->whereIn($model->getKeyName(), $model::search($this->data->getSearch())->keys());
142151

143152
return $subQuery;
144153
})
@@ -148,6 +157,7 @@ protected function applySearch(Builder $query, RequestPayload $payload): Builder
148157
$subQuery
149158
->where(fn ($query) => $this
150159
->getColumns()
160+
->filter(fn (Column $column) => ! Str::of($column->getName())->contains('.'))
151161
->each(fn (Column $column) => $query->orWhere($column->getName(), 'like', "%{$payload->getSearch()}%"))
152162
);
153163

@@ -279,16 +289,14 @@ public function response(?Builder $queryOrModel = null): array|Collection
279289
$this->withQuery($queryOrModel);
280290
}
281291

282-
$query = $this->getQuery();
283-
284-
$this->query = tap($query, function () use (&$query) {
292+
$this->query = tap($this->getQuery(), function ($query) {
285293
$this->applyQueryFilters($query, $this->data);
286294
$this->applyQuerySorting($query, $this->data);
287295
$this->applySearch($query, $this->data);
288296
});
289297

290298
/** @var LengthAwarePaginator $collection */
291-
$collection = $query->paginate($this->data->getPerPage());
299+
$collection = $this->query->paginate($this->data->getPerPage());
292300

293301
if (method_exists($this, 'transform')) {
294302
$collection->transform(fn ($record) => $this->transform($record));

0 commit comments

Comments
 (0)