From 3eb224d3e3bae198512bb4cabaa796eefec1f0dc Mon Sep 17 00:00:00 2001 From: antoine Date: Mon, 8 Dec 2025 18:04:35 +0100 Subject: [PATCH 1/3] Allow default filter value for all filters --- src/EntityList/Filters/HiddenFilter.php | 5 + src/Filters/AutocompleteRemoteFilter.php | 82 +----------- src/Filters/AutocompleteRemoteFilterTrait.php | 99 ++++++++++++++ .../AutocompleteRemoteRequiredFilter.php | 4 +- src/Filters/CheckFilter.php | 5 + src/Filters/DateRangeFilter.php | 113 +--------------- src/Filters/DateRangeFilterTrait.php | 123 ++++++++++++++++++ src/Filters/DateRangeRequiredFilter.php | 4 +- src/Filters/Filter.php | 1 + .../FilterContainer/FilterContainer.php | 8 -- src/Filters/SelectFilter.php | 82 +----------- src/Filters/SelectFilterTrait.php | 94 +++++++++++++ src/Filters/SelectMultipleFilter.php | 9 +- src/Filters/SelectRequiredFilter.php | 4 +- 14 files changed, 352 insertions(+), 281 deletions(-) create mode 100644 src/Filters/AutocompleteRemoteFilterTrait.php create mode 100644 src/Filters/DateRangeFilterTrait.php create mode 100644 src/Filters/SelectFilterTrait.php diff --git a/src/EntityList/Filters/HiddenFilter.php b/src/EntityList/Filters/HiddenFilter.php index 8546589e4..1a3f3bf03 100644 --- a/src/EntityList/Filters/HiddenFilter.php +++ b/src/EntityList/Filters/HiddenFilter.php @@ -27,4 +27,9 @@ public function toArray(): array { return []; } + + public function defaultValue(): mixed + { + return null; + } } diff --git a/src/Filters/AutocompleteRemoteFilter.php b/src/Filters/AutocompleteRemoteFilter.php index 4b21c8b9a..02a58b087 100644 --- a/src/Filters/AutocompleteRemoteFilter.php +++ b/src/Filters/AutocompleteRemoteFilter.php @@ -2,90 +2,12 @@ namespace Code16\Sharp\Filters; -use Code16\Sharp\Enums\FilterType; - abstract class AutocompleteRemoteFilter extends Filter { - private bool $isMaster = false; - private int $debounceDelay = 300; - private int $searchMinChars = 1; - private array $cache = []; - - final public function configureMaster(bool $isMaster = true): self - { - $this->isMaster = $isMaster; - - return $this; - } + use AutocompleteRemoteFilterTrait; - final public function configureDebounceDelay(int $delay): self + public function defaultValue(): mixed { - $this->debounceDelay = $delay; - - return $this; - } - - final public function configureSearchMinChars(int $minChars): self - { - $this->searchMinChars = $minChars; - - return $this; - } - - public function fromQueryParam($value): mixed - { - if ($value) { - if ($this->cache[$value] ?? null) { - return $this->cache[$value]; - } - if ($label = $this->valueLabelFor($value)) { - return $this->cache[$value] = [ - 'id' => $value, - 'label' => $label, - ]; - } - } - return null; } - - public function toQueryParam($value): mixed - { - return is_array($value) ? $value['id'] : $value; - } - - public function toArray(): array - { - return parent::buildArray([ - 'type' => FilterType::AutocompleteRemote->value, - 'master' => $this->isMaster, - 'required' => $this instanceof AutocompleteRemoteRequiredFilter, - // 'multiple' => $this instanceof AutocompleteRemoteMultipleFilter, - 'debounceDelay' => $this->debounceDelay, - 'searchMinChars' => $this->searchMinChars, - ]); - } - - final public function format(array $values) - { - if (! is_array(collect($values)->first())) { - return collect($values) - ->map(function ($label, $id) { - return compact('id', 'label'); - }) - ->values() - ->all(); - } - - return $values; - } - - public function formatRawValue(mixed $value): mixed - { - return $value ? $value['id'] : null; - } - - abstract public function values(string $query): array; - - abstract public function valueLabelFor(string $id): ?string; } diff --git a/src/Filters/AutocompleteRemoteFilterTrait.php b/src/Filters/AutocompleteRemoteFilterTrait.php new file mode 100644 index 000000000..324cf2284 --- /dev/null +++ b/src/Filters/AutocompleteRemoteFilterTrait.php @@ -0,0 +1,99 @@ +isMaster = $isMaster; + + return $this; + } + + final public function configureDebounceDelay(int $delay): self + { + $this->debounceDelay = $delay; + + return $this; + } + + final public function configureSearchMinChars(int $minChars): self + { + $this->searchMinChars = $minChars; + + return $this; + } + + public function fromQueryParam($value): mixed + { + if ($value) { + if ($this->cache[$value] ?? null) { + return $this->cache[$value]; + } + if ($label = $this->valueLabelFor($value)) { + return $this->cache[$value] = [ + 'id' => $value, + 'label' => $label, + ]; + } + } + + return null; + } + + public function toQueryParam($value): mixed + { + return is_array($value) ? $value['id'] : $value; + } + + public function defaultValue(): mixed + { + return null; + } + + public function toArray(): array + { + return parent::buildArray([ + 'type' => FilterType::AutocompleteRemote->value, + 'master' => $this->isMaster, + 'required' => $this instanceof AutocompleteRemoteRequiredFilter, + // 'multiple' => $this instanceof AutocompleteRemoteMultipleFilter, + 'debounceDelay' => $this->debounceDelay, + 'searchMinChars' => $this->searchMinChars, + ]); + } + + final public function format(array $values) + { + if (! is_array(collect($values)->first())) { + return collect($values) + ->map(function ($label, $id) { + return compact('id', 'label'); + }) + ->values() + ->all(); + } + + return $values; + } + + public function formatRawValue(mixed $value): mixed + { + return $value ? $value['id'] : null; + } + + abstract public function values(string $query): array; + + abstract public function valueLabelFor(string $id): ?string; +} diff --git a/src/Filters/AutocompleteRemoteRequiredFilter.php b/src/Filters/AutocompleteRemoteRequiredFilter.php index baa285055..28eede855 100644 --- a/src/Filters/AutocompleteRemoteRequiredFilter.php +++ b/src/Filters/AutocompleteRemoteRequiredFilter.php @@ -2,7 +2,9 @@ namespace Code16\Sharp\Filters; -abstract class AutocompleteRemoteRequiredFilter extends AutocompleteRemoteFilter +abstract class AutocompleteRemoteRequiredFilter extends Filter { + use AutocompleteRemoteFilterTrait; + abstract public function defaultValue(): mixed; } diff --git a/src/Filters/CheckFilter.php b/src/Filters/CheckFilter.php index eb8ad0048..e6acd6390 100644 --- a/src/Filters/CheckFilter.php +++ b/src/Filters/CheckFilter.php @@ -16,6 +16,11 @@ public function toQueryParam($value): mixed return $value; } + public function defaultValue(): bool + { + return false; + } + public function toArray(): array { return parent::buildArray([ diff --git a/src/Filters/DateRangeFilter.php b/src/Filters/DateRangeFilter.php index 115638444..a27d0d3b1 100644 --- a/src/Filters/DateRangeFilter.php +++ b/src/Filters/DateRangeFilter.php @@ -2,119 +2,12 @@ namespace Code16\Sharp\Filters; -use Carbon\Carbon; -use Code16\Sharp\Enums\FilterType; -use Code16\Sharp\Filters\DateRange\DateRangeFilterValue; -use Code16\Sharp\Filters\DateRange\DateRangePreset; - abstract class DateRangeFilter extends Filter { - private string $dateFormat = 'YYYY-MM-DD'; - private bool $isMondayFirst = true; - - /** @var DateRangePreset[] */ - private ?array $presets = null; - - final public function configureDateFormat(string $dateFormat): self - { - $this->dateFormat = $dateFormat; - - return $this; - } - - final public function configureMondayFirst(bool $isMondayFirst = true): self - { - $this->isMondayFirst = $isMondayFirst; - - return $this; - } - - final public function configureShowPresets(bool $showPresets = true, ?array $presets = null): self - { - if ($showPresets) { - $this->presets = $presets !== null - ? $presets - : [ - DateRangePreset::today(), - DateRangePreset::yesterday(), - DateRangePreset::last7days(), - DateRangePreset::last30days(), - DateRangePreset::last365days(), - DateRangePreset::thisMonth(), - DateRangePreset::lastMonth(), - DateRangePreset::thisYear(), - DateRangePreset::lastYear(), - ]; - } else { - $this->presets = []; - } - - return $this; - } - - final public function getDateFormat(): string - { - return $this->dateFormat; - } - - final public function isMondayFirst(): bool - { - return $this->isMondayFirst; - } - - /** - * @internal - */ - final public function fromQueryParam($value): ?array - { - if (! $value) { - return null; - } - - [$start, $end] = explode('..', $value); - $start = Carbon::createFromFormat('Ymd', $start)->startOfDay(); - $end = Carbon::createFromFormat('Ymd', $end)->endOfDay(); - - return [ - 'start' => $start->format('Y-m-d'), - 'end' => $end->format('Y-m-d'), - 'formatted' => [ - 'start' => $start->isoFormat($this->getDateFormat()), - 'end' => $end->isoFormat($this->getDateFormat()), - ], - ]; - } - - /** - * @internal - */ - final public function toQueryParam($value): ?string - { - if (! $value) { - return null; - } - - return sprintf( - '%s..%s', - Carbon::parse($value['start'])->format('Ymd'), - Carbon::parse($value['end'])->format('Ymd'), - ); - } - - public function toArray(): array - { - return parent::buildArray([ - 'type' => FilterType::DateRange->value, - 'required' => $this instanceof DateRangeRequiredFilter, - 'mondayFirst' => $this->isMondayFirst(), - 'presets' => collect($this->presets) - ->map(fn (DateRangePreset $preset) => $preset->toArray()) - ->toArray(), - ]); - } + use DateRangeFilterTrait; - public function formatRawValue(mixed $value): DateRangeFilterValue + public function defaultValue(): mixed { - return new DateRangeFilterValue(Carbon::parse($value['start']), Carbon::parse($value['end'])); + return null; } } diff --git a/src/Filters/DateRangeFilterTrait.php b/src/Filters/DateRangeFilterTrait.php new file mode 100644 index 000000000..1c13f7f94 --- /dev/null +++ b/src/Filters/DateRangeFilterTrait.php @@ -0,0 +1,123 @@ +dateFormat = $dateFormat; + + return $this; + } + + final public function configureMondayFirst(bool $isMondayFirst = true): self + { + $this->isMondayFirst = $isMondayFirst; + + return $this; + } + + final public function configureShowPresets(bool $showPresets = true, ?array $presets = null): self + { + if ($showPresets) { + $this->presets = $presets !== null + ? $presets + : [ + DateRangePreset::today(), + DateRangePreset::yesterday(), + DateRangePreset::last7days(), + DateRangePreset::last30days(), + DateRangePreset::last365days(), + DateRangePreset::thisMonth(), + DateRangePreset::lastMonth(), + DateRangePreset::thisYear(), + DateRangePreset::lastYear(), + ]; + } else { + $this->presets = []; + } + + return $this; + } + + final public function getDateFormat(): string + { + return $this->dateFormat; + } + + final public function isMondayFirst(): bool + { + return $this->isMondayFirst; + } + + /** + * @internal + */ + final public function fromQueryParam($value): ?array + { + if (! $value) { + return null; + } + + [$start, $end] = explode('..', $value); + $start = Carbon::createFromFormat('Ymd', $start)->startOfDay(); + $end = Carbon::createFromFormat('Ymd', $end)->endOfDay(); + + return [ + 'start' => $start->format('Y-m-d'), + 'end' => $end->format('Y-m-d'), + 'formatted' => [ + 'start' => $start->isoFormat($this->getDateFormat()), + 'end' => $end->isoFormat($this->getDateFormat()), + ], + ]; + } + + /** + * @internal + */ + final public function toQueryParam($value): ?string + { + if (! $value) { + return null; + } + + return sprintf( + '%s..%s', + Carbon::parse($value['start'])->format('Ymd'), + Carbon::parse($value['end'])->format('Ymd'), + ); + } + + public function toArray(): array + { + return parent::buildArray([ + 'type' => FilterType::DateRange->value, + 'required' => $this instanceof DateRangeRequiredFilter, + 'mondayFirst' => $this->isMondayFirst(), + 'presets' => collect($this->presets) + ->map(fn (DateRangePreset $preset) => $preset->toArray()) + ->toArray(), + ]); + } + + public function formatRawValue(mixed $value): DateRangeFilterValue + { + return new DateRangeFilterValue(Carbon::parse($value['start']), Carbon::parse($value['end'])); + } +} diff --git a/src/Filters/DateRangeRequiredFilter.php b/src/Filters/DateRangeRequiredFilter.php index 82b9193a7..7ef3320bf 100644 --- a/src/Filters/DateRangeRequiredFilter.php +++ b/src/Filters/DateRangeRequiredFilter.php @@ -2,8 +2,10 @@ namespace Code16\Sharp\Filters; -abstract class DateRangeRequiredFilter extends DateRangeFilter +abstract class DateRangeRequiredFilter extends Filter { + use DateRangeFilterTrait; + /** * @return array Expected format: ["start" => Carbon::yesterday(), "end" => Carbon::today()] */ diff --git a/src/Filters/Filter.php b/src/Filters/Filter.php index d94db43c1..b419d5715 100644 --- a/src/Filters/Filter.php +++ b/src/Filters/Filter.php @@ -63,4 +63,5 @@ protected function buildArray(array $childArray): array abstract public function toArray(): array; abstract public function fromQueryParam($value): mixed; abstract public function toQueryParam($value): mixed; + abstract public function defaultValue(): mixed; } diff --git a/src/Filters/FilterContainer/FilterContainer.php b/src/Filters/FilterContainer/FilterContainer.php index cfd0417e8..3d867dd4a 100644 --- a/src/Filters/FilterContainer/FilterContainer.php +++ b/src/Filters/FilterContainer/FilterContainer.php @@ -3,14 +3,11 @@ namespace Code16\Sharp\Filters\FilterContainer; use Code16\Sharp\Exceptions\SharpException; -use Code16\Sharp\Filters\AutocompleteRemoteRequiredFilter; -use Code16\Sharp\Filters\DateRangeRequiredFilter; use Code16\Sharp\Filters\Filter; use Code16\Sharp\Filters\FilterContainer\Concerns\BuildsFiltersConfigArray; use Code16\Sharp\Filters\FilterContainer\Concerns\HandlesFiltersInQueryParams; use Code16\Sharp\Filters\FilterContainer\Concerns\HandlesFiltersInSession; use Code16\Sharp\Filters\FilterContainer\Concerns\ProvidesFilterValuesToFront; -use Code16\Sharp\Filters\SelectRequiredFilter; use Illuminate\Support\Collection; class FilterContainer @@ -95,11 +92,6 @@ public function getDefaultFilterValues(): Collection { return $this->getFilterHandlers() ->flatten() - ->whereInstanceOf([ - SelectRequiredFilter::class, - DateRangeRequiredFilter::class, - AutocompleteRemoteRequiredFilter::class, - ]) ->mapWithKeys(function (Filter $handler) { return [ $handler->getKey() => $handler->toQueryParam($handler->defaultValue()), diff --git a/src/Filters/SelectFilter.php b/src/Filters/SelectFilter.php index a95050256..a7efd50c9 100644 --- a/src/Filters/SelectFilter.php +++ b/src/Filters/SelectFilter.php @@ -2,88 +2,12 @@ namespace Code16\Sharp\Filters; -use Code16\Sharp\Enums\FilterType; - abstract class SelectFilter extends Filter { - private bool $isMaster = false; - private bool $isSearchable = false; - private array $searchKeys = ['label']; - - final public function isMaster(): bool - { - return $this->isMaster; - } - - final public function isSearchable(): bool - { - return $this->isSearchable; - } - - final public function getSearchKeys(): array - { - return $this->searchKeys; - } - - final public function configureSearchable(bool $isSearchable = true): self - { - $this->isSearchable = $isSearchable; - - return $this; - } - - final public function configureSearchKeys(array $searchKeys): self - { - $this->searchKeys = $searchKeys; - - return $this; - } - - final public function configureMaster(bool $isMaster = true): self - { - $this->isMaster = $isMaster; - - return $this; - } + use SelectFilterTrait; - public function fromQueryParam($value): mixed + public function defaultValue(): mixed { - return $value; + return null; } - - public function toQueryParam($value): mixed - { - return $value; - } - - public function toArray(): array - { - return parent::buildArray([ - 'type' => FilterType::Select->value, - 'multiple' => $this instanceof SelectMultipleFilter, - 'required' => $this instanceof SelectRequiredFilter, - 'values' => $this->formattedValues(), - 'master' => $this->isMaster(), - 'searchable' => $this->isSearchable(), - 'searchKeys' => $this->getSearchKeys(), - ]); - } - - protected function formattedValues(): array - { - $values = $this->values(); - - if (! is_array(collect($values)->first())) { - return collect($values) - ->map(function ($label, $id) { - return compact('id', 'label'); - }) - ->values() - ->all(); - } - - return $values; - } - - abstract public function values(): array; } diff --git a/src/Filters/SelectFilterTrait.php b/src/Filters/SelectFilterTrait.php new file mode 100644 index 000000000..1f681eeed --- /dev/null +++ b/src/Filters/SelectFilterTrait.php @@ -0,0 +1,94 @@ +isMaster; + } + + final public function isSearchable(): bool + { + return $this->isSearchable; + } + + final public function getSearchKeys(): array + { + return $this->searchKeys; + } + + final public function configureSearchable(bool $isSearchable = true): self + { + $this->isSearchable = $isSearchable; + + return $this; + } + + final public function configureSearchKeys(array $searchKeys): self + { + $this->searchKeys = $searchKeys; + + return $this; + } + + final public function configureMaster(bool $isMaster = true): self + { + $this->isMaster = $isMaster; + + return $this; + } + + public function fromQueryParam($value): mixed + { + return $value; + } + + public function toQueryParam($value): mixed + { + return $value; + } + + public function toArray(): array + { + return parent::buildArray([ + 'type' => FilterType::Select->value, + 'multiple' => $this instanceof SelectMultipleFilter, + 'required' => $this instanceof SelectRequiredFilter, + 'values' => $this->formattedValues(), + 'master' => $this->isMaster(), + 'searchable' => $this->isSearchable(), + 'searchKeys' => $this->getSearchKeys(), + ]); + } + + protected function formattedValues(): array + { + $values = $this->values(); + + if (! is_array(collect($values)->first())) { + return collect($values) + ->map(function ($label, $id) { + return compact('id', 'label'); + }) + ->values() + ->all(); + } + + return $values; + } + + abstract public function values(): array; +} diff --git a/src/Filters/SelectMultipleFilter.php b/src/Filters/SelectMultipleFilter.php index ab4410ecb..3bbf4a5c3 100644 --- a/src/Filters/SelectMultipleFilter.php +++ b/src/Filters/SelectMultipleFilter.php @@ -4,8 +4,10 @@ use Illuminate\Support\Arr; -abstract class SelectMultipleFilter extends SelectFilter +abstract class SelectMultipleFilter extends Filter { + use SelectFilterTrait; + public function fromQueryParam($value): array { return $value !== null @@ -21,4 +23,9 @@ public function toQueryParam($value): ?string ? implode(',', $values) : null; } + + public function defaultValue(): array + { + return []; + } } diff --git a/src/Filters/SelectRequiredFilter.php b/src/Filters/SelectRequiredFilter.php index d39b68564..616d46ca9 100644 --- a/src/Filters/SelectRequiredFilter.php +++ b/src/Filters/SelectRequiredFilter.php @@ -2,7 +2,9 @@ namespace Code16\Sharp\Filters; -abstract class SelectRequiredFilter extends SelectFilter +abstract class SelectRequiredFilter extends Filter { + use SelectFilterTrait; + abstract public function defaultValue(): mixed; } From 5da1957c2fc005cb3d06f668b68c1bfcdedbf740 Mon Sep 17 00:00:00 2001 From: antoine Date: Mon, 8 Dec 2025 18:07:25 +0100 Subject: [PATCH 2/3] cleanup --- src/Filters/SelectFilterTrait.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Filters/SelectFilterTrait.php b/src/Filters/SelectFilterTrait.php index 1f681eeed..df9938908 100644 --- a/src/Filters/SelectFilterTrait.php +++ b/src/Filters/SelectFilterTrait.php @@ -6,8 +6,6 @@ /** * @internal - * - * @mixin Filter */ trait SelectFilterTrait { From 4ebca856588658b4317040c4cb3f6793a096e791 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 9 Dec 2025 08:52:04 +0100 Subject: [PATCH 3/3] Add unit test --- .../EntityList/SharpEntityListFilterTest.php | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/Unit/EntityList/SharpEntityListFilterTest.php b/tests/Unit/EntityList/SharpEntityListFilterTest.php index 6e78ebfa8..4002dbbc5 100644 --- a/tests/Unit/EntityList/SharpEntityListFilterTest.php +++ b/tests/Unit/EntityList/SharpEntityListFilterTest.php @@ -319,6 +319,68 @@ public function values(): array expect($list->listConfig()['filters']['_root'][0]['searchKeys'])->toEqual(['a', 'b']); }); +it('allows unrequired filter to have a default value', function () { + $list = new class() extends FakeSharpEntityList + { + public function getFilters(): array + { + return [ + new class() extends SelectFilter + { + public function buildFilterConfig(): void + { + $this->configureKey('select'); + } + + public function values(): array + { + return ['A' => 'A', 'B' => 'B']; + } + + public function defaultValue(): mixed + { + return 'B'; + } + }, + + new class() extends DateRangeFilter + { + public function buildFilterConfig(): void + { + $this->configureKey('date'); + } + + public function defaultValue(): mixed + { + return [ + 'start' => today()->subDay(), + 'end' => today(), + ]; + } + }, + ]; + } + }; + + $list->buildListConfig(); + $filterValues = $list->filterContainer()->getCurrentFilterValuesForFront(null); + + expect($filterValues['default']['select'])->toEqual('B') + ->and($filterValues['current']['select'])->toEqual('B') + ->and($filterValues['default']['date'])->toMatchArray([ + 'start' => today()->subDay()->format('Y-m-d'), + 'end' => today()->format('Y-m-d'), + ]) + ->and($filterValues['current']['date'])->toMatchArray([ + 'start' => today()->subDay()->format('Y-m-d'), + 'end' => today()->format('Y-m-d'), + ]) + ->and($filterValues['valuated'])->toEqual([ + 'select' => false, + 'date' => false, + ]); +}); + it('allows to declare a filter as retained and to set its default value', function () { $list = new class() extends FakeSharpEntityList {