Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion demo/app/Sharp/Categories/CategoryList.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use App\Models\Category;
use App\Sharp\Categories\Commands\CleanUnusedCategoriesCommand;
use App\Sharp\Utils\Filters\PostFilter;
use Code16\Sharp\EntityList\Eloquent\SimpleEloquentReorderHandler;
use Code16\Sharp\EntityList\Fields\EntityListField;
use Code16\Sharp\EntityList\Fields\EntityListFieldsContainer;
Expand Down Expand Up @@ -50,16 +51,25 @@ public function buildFilterConfig(): void
->configureLabel('Orphan categories only.');
}
},
PostFilter::class,
];
}

public function getListData(): array|Arrayable
{
$categories = Category::withCount('posts')
$postIds = $this->queryParams->filterFor(PostFilter::class);

$categories = Category::withCount([
'posts' => fn ($q) => $q->when($postIds, fn ($q) => $q->whereKey($postIds)),
])
->orderBy('order')
->when(
$this->queryParams->filterFor('orphan'),
fn ($q) => $q->having('posts_count', 0)
)
->when(
$postIds,
fn ($q) => $q->whereHas('posts', fn ($q) => $q->whereKey($postIds))
);

return $this->transform($categories->get());
Expand Down
39 changes: 39 additions & 0 deletions demo/app/Sharp/Utils/Filters/PostFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace App\Sharp\Utils\Filters;

use App\Models\Post;
use Code16\Sharp\Filters\AutocompleteRemoteMultipleFilter;

class PostFilter extends AutocompleteRemoteMultipleFilter
{
public function buildFilterConfig(): void
{
$this->configureLabel('Posts');
}

public function values(string $query): array
{
return Post::query()
->when($query, function ($builder) use ($query) {
$builder->where(function ($builder) use ($query) {
$builder->where('title->en', 'like', "%$query%")
->orWhere('title->fr', 'like', "%$query%");
});
})
->orderBy('title->en')
->limit(20)
->get()
->mapWithKeys(fn (Post $post) => [$post->id => $post->getTranslation('title', 'en')])
->all();
}

public function valueLabelsFor(array $ids): array
{
return Post::query()
->whereKey($ids)
->get()
->mapWithKeys(fn (Post $post) => [$post->id => $post->getTranslation('title', 'en')])
->all();
}
}
46 changes: 46 additions & 0 deletions docs/guide/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,52 @@ class ProductCategoryFilter extends AutocompleteRemoteFilter

The `values()` method must return an `[{id} => {label}]` array. The `valueLabelFor()` method is used to display the label in the dropdown for the selected id.

### Multiple remote autocomplete filter

For a large set of options, use `Code16\Sharp\Filters\AutocompleteRemoteMultipleFilter`. It combines remote searching with a multiple selection, without sending the complete option list to the browser.

```php
use Code16\Sharp\Filters\AutocompleteRemoteMultipleFilter;

class ProductFilter extends AutocompleteRemoteMultipleFilter
{
public function buildFilterConfig(): void
{
$this
->configureKey('products')
->configureLabel('Products')
->configureSearchMinChars(2)
->configureDebounceDelay(300);
}

public function values(string $query): array
{
return Product::query()
->where('name', 'like', "%{$query}%")
->limit(50)
->pluck('name', 'id')
->all();
}

public function valueLabelsFor(array $ids): array
{
return Product::query()
->whereKey($ids)
->pluck('name', 'id')
->all();
}
}
```

Both methods must return an `[id => label]` array. `values()` is called only for the typed search and should limit its result set. `valueLabelsFor()` is called once with the selected IDs when Sharp restores the filter from the URL or session; this avoids one query per selected value.

In the Entity List, the selected value is an array of IDs:

```php
if ($productIds = $this->queryParams->filterFor(ProductFilter::class)) {
$products->whereIn('id', $productIds);
}
```

## Required filters

Expand Down
8 changes: 6 additions & 2 deletions resources/js/Pages/Dashboard/Dashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,25 @@
});

function onFilterChange(filter: FilterData, value: FilterData['value']) {
const filterValues = filters.nextValues(filter, value);
filters.setCurrentValues(filterValues);
router.post(
route('code16.sharp.dashboard.filters.store', { dashboardKey }),
{
filterValues: filters.nextValues(filter, value),
filterValues,
query: parseQuery(location.search),
},
{ preserveState: true, preserveScroll: true },
);
}

function onFiltersReset(resettedFilters: FilterData[]) {
const filterValues = filters.defaultValues(resettedFilters);
filters.setCurrentValues(filterValues);
router.post(
route('code16.sharp.dashboard.filters.store', { dashboardKey }),
{
filterValues: filters.defaultValues(resettedFilters),
filterValues,
query: parseQuery(location.search),
},
{ preserveState: true, preserveScroll: true },
Expand Down
8 changes: 6 additions & 2 deletions resources/js/Pages/EntityList/EntityList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,25 @@
}

function onFilterChange(filter: FilterData, value: FilterData['value']) {
const filterValues = filters.nextValues(filter, value);
filters.setCurrentValues(filterValues);
router.post(
route('code16.sharp.list.filters.store', { entityKey }),
{
filterValues: filters.nextValues(filter, value),
filterValues,
query: entityList.value.query,
},
{ preserveState: true, preserveScroll: false }
);
}

function onReset() {
const filterValues = filters.defaultValues(filters.rootFilters);
filters.setCurrentValues(filterValues);
router.post(
route('code16.sharp.list.filters.store', { entityKey }),
{
filterValues: filters.defaultValues(filters.rootFilters),
filterValues,
query: { ...entityList.value.query, search: null },
},
{ preserveState: true, preserveScroll: false }
Expand Down
10 changes: 10 additions & 0 deletions resources/js/filters/FilterManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ export class FilterManager {
return { ...this.currentValues, [filter.key]: value };
}

/**
* Optimistically apply values before the server responds, so that successive changes don't build on stale values
*/
setCurrentValues(values: FilterValues) {
this.state.filterValues = {
...this.filterValues,
current: { ...this.currentValues, ...values },
};
}

defaultValues(filters: FilterData[]) {
return Object.fromEntries(
filters.map(filter => [filter.key, this.filterValues.default[filter.key]])
Expand Down
Loading
Loading