diff --git a/resources/lang/en/ui.php b/resources/lang/en/ui.php index 2bfaf49..ec8c63e 100644 --- a/resources/lang/en/ui.php +++ b/resources/lang/en/ui.php @@ -5,4 +5,5 @@ 'default' => 'Default', 'all' => 'All', 'value' => 'Value', + 'to' => 'to', ]; diff --git a/resources/views/livewire/filters/lf-dual-range.blade.php b/resources/views/livewire/filters/lf-dual-range.blade.php new file mode 100644 index 0000000..5342bda --- /dev/null +++ b/resources/views/livewire/filters/lf-dual-range.blade.php @@ -0,0 +1,73 @@ +
+
+
+
+
+ + {{ __('statamic-livewire-filters::ui.to') }} + +
+
+
+
+ +@script + +@endscript \ No newline at end of file diff --git a/src/Http/Livewire/LfDualRangeFilter.php b/src/Http/Livewire/LfDualRangeFilter.php new file mode 100644 index 0000000..332046b --- /dev/null +++ b/src/Http/Livewire/LfDualRangeFilter.php @@ -0,0 +1,93 @@ +condition = 'dual-range'; + $this->selectedMin = $defaultMin ?? $this->min; + $this->selectedMax = $defaultMax ?? $this->max; + } + + public function dispatchEvent() + { + $this->dispatch('filter-updated', + field: $this->field, + condition: $this->condition, + payload: [ + 'min' => $this->selectedMin, + 'max' => $this->selectedMax, + ], + command: 'replace', + modifier: $this->modifier, + ) + ->to(LivewireCollection::class); + } + + public function updatedSelectedMin($value) + { + // Ensure min doesn't exceed max - minRange + if ($value > $this->selectedMax - $this->minRange) { + $this->selectedMin = $this->selectedMax - $this->minRange; + } + $this->dispatchEvent(); + } + + public function updatedSelectedMax($value) + { + // Ensure max doesn't go below min + minRange + if ($value < $this->selectedMin + $this->minRange) { + $this->selectedMax = $this->selectedMin + $this->minRange; + } + $this->dispatchEvent(); + } + + // #[On('livewire:initialized')] + // public function livewireComponentReady() + // { + // $this->dispatchEvent(); + // } + + #[On('preset-params')] + public function setPresetSort($params) + { + $paramKey = $this->getParamKey(); + if (isset($params[$paramKey]['min'])) { + $this->selectedMin = $params[$paramKey]['min']; + } + if (isset($params[$paramKey]['max'])) { + $this->selectedMax = $params[$paramKey]['max']; + } + } + + public function render() + { + return view('statamic-livewire-filters::livewire.filters.'.$this->view); + } +} diff --git a/src/Http/Livewire/LivewireCollection.php b/src/Http/Livewire/LivewireCollection.php index ae8b174..0d155f5 100644 --- a/src/Http/Livewire/LivewireCollection.php +++ b/src/Http/Livewire/LivewireCollection.php @@ -55,6 +55,11 @@ public function filterUpdated($field, $condition, $payload, $command, $modifier) return; } + if ($condition === 'dual-range') { + $this->handleDualRangeCondition($field, $payload, $command, $modifier); + + return; + } $this->handleCondition($field, $condition, $payload, $command); } diff --git a/src/Http/Livewire/Traits/HandleParams.php b/src/Http/Livewire/Traits/HandleParams.php index ddb3e55..89da163 100644 --- a/src/Http/Livewire/Traits/HandleParams.php +++ b/src/Http/Livewire/Traits/HandleParams.php @@ -127,6 +127,36 @@ protected function handleQueryScopeCondition($field, $payload, $command, $modifi $this->dispatchParamsUpdated(); } + protected function handleDualRangeCondition($field, $payload, $command, $modifier) + { + $minModifier = 'gte'; + $maxModifier = 'lte'; + + // If the modifier is set, we need to extract the min and max modifiers + if ($modifier !== null) { + [$minModifier, $maxModifier] = explode('|', $modifier); + } + + $minParamKey = $field.':'.$minModifier; + $maxParamKey = $field.':'.$maxModifier; + + switch ($command) { + case 'replace': + $this->params[$minParamKey] = $payload['min']; + $this->params[$maxParamKey] = $payload['max']; + break; + + case 'clear': + unset($this->params[$minParamKey]); + unset($this->params[$maxParamKey]); + break; + + default: + throw new CommandNotFoundException($command); + } + $this->dispatchParamsUpdated(); + } + protected function runCommand($command, $paramKey, $value) { switch ($command) { diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 0d370e4..88a3b65 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -6,6 +6,7 @@ use Livewire\Livewire; use Reach\StatamicLivewireFilters\Http\Livewire\LfCheckboxFilter; use Reach\StatamicLivewireFilters\Http\Livewire\LfDateFilter; +use Reach\StatamicLivewireFilters\Http\Livewire\LfDualRangeFilter; use Reach\StatamicLivewireFilters\Http\Livewire\LfRadioFilter; use Reach\StatamicLivewireFilters\Http\Livewire\LfRangeFilter; use Reach\StatamicLivewireFilters\Http\Livewire\LfSelectFilter; @@ -46,6 +47,7 @@ public function bootAddon() Livewire::component('livewire-collection', LivewireCollectionComponent::class); Livewire::component('lf-checkbox-filter', LfCheckboxFilter::class); Livewire::component('lf-date-filter', LfDateFilter::class); + Livewire::component('lf-dual-range-filter', LfDualRangeFilter::class); Livewire::component('lf-radio-filter', LfRadioFilter::class); Livewire::component('lf-range-filter', LfRangeFilter::class); Livewire::component('lf-text-filter', LfTextFilter::class); diff --git a/tests/Feature/LfDualRangeFilterTest.php b/tests/Feature/LfDualRangeFilterTest.php new file mode 100644 index 0000000..d1ed13f --- /dev/null +++ b/tests/Feature/LfDualRangeFilterTest.php @@ -0,0 +1,229 @@ +collection = Facades\Collection::make('yachts')->save(); + $this->blueprint = Facades\Blueprint::make()->setContents([ + 'sections' => [ + 'main' => [ + 'fields' => [ + [ + 'handle' => 'title', + 'field' => [ + 'type' => 'text', + 'display' => 'Title', + ], + ], + [ + 'handle' => 'cabins', + 'field' => [ + 'type' => 'text', + 'display' => 'Cabins', + 'listable' => 'hidden', + ], + ], + [ + 'handle' => 'year', + 'field' => [ + 'type' => 'text', + 'display' => 'Year', + 'listable' => 'hidden', + ], + ], + ], + ], + ], + ]); + $this->blueprint->setHandle('yachts')->setNamespace('collections.'.$this->collection->handle())->save(); + + $this->makeEntry($this->collection, 'yacht-a')->set('title', 'Luxury Yacht A')->set('cabins', 4)->set('year', 2020)->save(); + $this->makeEntry($this->collection, 'yacht-b')->set('title', 'Luxury Yacht B')->set('cabins', 6)->set('year', 2022)->save(); + $this->makeEntry($this->collection, 'yacht-c')->set('title', 'Luxury Yacht C')->set('cabins', 8)->set('year', 2024)->save(); + } + + #[Test] + public function it_renders_the_component_with_correct_min_and_max_values() + { + Livewire::test(LfDualRangeFilter::class, [ + 'field' => 'cabins', + 'blueprint' => 'yachts.yachts', + 'condition' => 'gte|lte', + 'min' => 2, + 'max' => 10, + 'defaultMin' => 4, + 'defaultMax' => 8, + 'minRange' => 2, + ]) + ->assertSet('selectedMin', 4) + ->assertSet('selectedMax', 8) + ->assertSet('minRange', 2) + ->assertSee('10'); + } + + #[Test] + public function it_throws_a_field_not_found_exception_if_the_field_doesnt_exist() + { + $this->expectExceptionMessage('Field [not-a-field] not found'); + + Livewire::test(LfDualRangeFilter::class, [ + 'field' => 'not-a-field', + 'blueprint' => 'yachts.yachts', + 'condition' => 'between', + 'min' => 2, + 'max' => 10, + 'defaultMin' => 4, + 'defaultMax' => 8, + ]); + } + + #[Test] + public function it_throws_a_blueprint_not_found_exception_if_the_blueprint_doesnt_exist() + { + $this->expectExceptionMessage('Blueprint [not-a-blueprint] not found'); + + Livewire::test(LfDualRangeFilter::class, [ + 'field' => 'cabins', + 'blueprint' => 'yachts.not-a-blueprint', + 'condition' => 'between', + 'min' => 2, + 'max' => 10, + 'defaultMin' => 4, + 'defaultMax' => 8, + ]); + } + + // #[Test] + // public function it_enforces_minimum_range_between_handles() + // { + // $component = Livewire::test(LfDualRangeFilter::class, [ + // 'field' => 'cabins', + // 'blueprint' => 'yachts.yachts', + // 'condition' => 'between', + // 'min' => 2, + // 'max' => 10, + // 'defaultMin' => 4, + // 'defaultMax' => 8, + // 'minRange' => 2, + // ]); + + // // Try to set min too close to max + // $component->set('selectedMin', 7) + // ->assertSet('selectedMin', 6) // Should be forced to max - minRange + // ->assertSet('selectedMax', 8); + + // // Try to set max too close to min + // $component->set('selectedMax', 5) + // ->assertSet('selectedMin', 4) + // ->assertSet('selectedMax', 6); // Should be forced to min + minRange + // } + + #[Test] + public function it_dispatches_filter_updated_event_when_values_change() + { + Livewire::test(LfDualRangeFilter::class, [ + 'field' => 'cabins', + 'blueprint' => 'yachts.yachts', + 'condition' => 'dual-range', + 'min' => 2, + 'max' => 10, + 'minRange' => 2, + ]) + ->set('selectedMin', 5) + ->assertDispatched('filter-updated', + field: 'cabins', + condition: 'dual-range', + payload: ['min' => 5, 'max' => 10], + command: 'replace', + ); + } + + #[Test] + public function collection_component_handles_dual_range_filter_events() + { + Livewire::test(LivewireCollection::class, ['params' => ['from' => 'yachts']]) + ->dispatch('filter-updated', + field: 'cabins', + condition: 'dual-range', + payload: ['min' => 5, 'max' => 10], + command: 'replace', + modifier: null, + ) + ->assertSet('params', [ + 'cabins:gte' => 5, + 'cabins:lte' => 10, + ]) + ->dispatch('filter-updated', + field: 'cabins', + condition: 'dual-range', + payload: ['min' => 5, 'max' => 8], + command: 'replace', + modifier: null, + ) + ->assertSet('params', [ + 'cabins:gte' => 5, + 'cabins:lte' => 8, + ]); + } + + #[Test] + public function collection_component_handles_different_conditions_by_modifier() + { + Livewire::test(LivewireCollection::class, ['params' => ['from' => 'yachts']]) + ->dispatch('filter-updated', + field: 'cabins', + condition: 'dual-range', + payload: ['min' => 5, 'max' => 10], + command: 'replace', + modifier: 'gt|lt', + ) + ->assertSet('params', [ + 'cabins:gt' => 5, + 'cabins:lt' => 10, + ]); + } + + // #[Test] + // public function it_loads_preset_params_correctly() + // { + // Livewire::test(LfDualRangeFilter::class, [ + // 'field' => 'cabins', + // 'blueprint' => 'yachts.yachts', + // 'condition' => 'between', + // 'min' => 2, + // 'max' => 10, + // 'defaultMin' => 4, + // 'defaultMax' => 8, + // ]) + // ->assertSet('selectedMin', 4) + // ->assertSet('selectedMax', 8) + // ->dispatch('preset-params', ['cabins:between' => ['min' => 5, 'max' => 7]]) + // ->assertSet('selectedMin', 5) + // ->assertSet('selectedMax', 7); + // } + + protected function makeEntry($collection, $slug) + { + return EntryFactory::id($slug)->collection($collection)->slug($slug)->make(); + } +}