diff --git a/src/Query/Scopes/Filters/Fields/Entries.php b/src/Query/Scopes/Filters/Fields/Entries.php index 0cd76e230a2..f70c4a786df 100644 --- a/src/Query/Scopes/Filters/Fields/Entries.php +++ b/src/Query/Scopes/Filters/Fields/Entries.php @@ -4,7 +4,6 @@ use Statamic\Facades; use Statamic\Support\Arr; -use Statamic\Support\Str; use function Statamic\trans as __; @@ -13,30 +12,28 @@ class Entries extends FieldtypeFilter public function fieldItems() { return [ - 'field' => [ - 'type' => 'select', - 'options' => [ - 'id' => __('ID'), - 'title' => __('Title'), - ], - 'default' => 'title', - ], 'operator' => [ 'type' => 'select', 'options' => [ - 'like' => __('Contains'), '=' => __('Is'), '!=' => __('Isn\'t'), 'null' => __('Empty'), 'not-null' => __('Not empty'), ], - 'default' => 'like', + 'default' => '=', ], 'value' => [ - 'type' => 'text', - 'placeholder' => __('Value'), - 'if' => [ - 'operator' => 'contains_any like, =, !=', + 'type' => 'entries', + 'max_items' => 1, + 'mode' => 'typeahead', + 'create' => false, + 'collections' => $this->fieldtype->config('collections'), + 'search_index' => $this->fieldtype->config('search_index'), + 'select_across_sites' => $this->fieldtype->config('select_across_sites'), + 'blueprints' => $this->fieldtype->config('blueprints'), + 'query_scopes' => $this->fieldtype->config('query_scopes'), + 'unless' => [ + 'operator' => 'contains_any null, not-null', ], 'required' => false, ], @@ -45,10 +42,7 @@ public function fieldItems() public function apply($query, $handle, $values) { - $config = $this->fieldtype->field()->config(); - $maxItems = $config['max_items'] ?? 0; $operator = $values['operator']; - $value = $values['value']; if (in_array($operator, ['null', 'not-null'])) { match ($operator) { @@ -59,56 +53,41 @@ public function apply($query, $handle, $values) return; } - if ($operator === 'like') { - $value = Str::ensureLeft($value, '%'); - $value = Str::ensureRight($value, '%'); - } - - if ($values['field'] == 'id') { - $maxItems === 1 - ? $query->where($handle, $operator, $value) - : $query->whereJsonContains($handle, $value); - + if (! $id = $values['value']) { return; } - $ids = Facades\Entry::query() - ->when($config['collections'] ?? null, fn ($query) => $query->whereIn('collection', $config['collections'])) - ->where($values['field'], $operator, $value) - ->get(['id']) - ->map(fn ($entry) => $entry->id()) - ->all(); + $single = $this->fieldtype->config('max_items') === 1; - if (empty($ids)) { - $maxItems === 1 - ? $query->where($handle, -1) - : $query->whereJsonContains($handle, [-1]); + if ($operator === '=') { + $single + ? $query->where($handle, $id) + : $query->whereJsonContains($handle, [$id]); return; } - if ($maxItems === 1) { - $query->whereIn($handle, $ids); - - return; - } - - $query->where(function ($subquery) use ($handle, $ids) { - foreach ($ids as $count => $id) { - $subquery->{$count == 0 ? 'whereJsonContains' : 'orWhereJsonContains'}($handle, [$id]); - } - }); + $single + ? $query->where($handle, '!=', $id) + : $query->whereJsonDoesntContain($handle, [$id]); } public function badge($values) { $field = $this->fieldtype->field()->display(); - $selectedField = $values['field']; $operator = $values['operator']; - $translatedField = Arr::get($this->fieldItems(), "field.options.{$selectedField}"); $translatedOperator = Arr::get($this->fieldItems(), "operator.options.{$operator}"); - $value = $values['value']; - return $field.' '.$translatedField.' '.strtolower($translatedOperator).' '.$value; + if (in_array($operator, ['null', 'not-null'])) { + return $field.' '.strtolower($translatedOperator); + } + + if (! $id = $values['value']) { + return null; + } + + $title = Facades\Entry::find($id)?->value('title') ?? $id; + + return $field.' '.strtolower($translatedOperator).' '.$title; } } diff --git a/tests/Query/FieldtypeFilterTest.php b/tests/Query/FieldtypeFilterTest.php index abf6a1419cf..e409a0e7d64 100644 --- a/tests/Query/FieldtypeFilterTest.php +++ b/tests/Query/FieldtypeFilterTest.php @@ -4,14 +4,18 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; +use Statamic\Facades\Collection; +use Statamic\Facades\Entry; use Statamic\Facades\Site; use Statamic\Facades\Taxonomy; use Statamic\Facades\Term; use Statamic\Fields\Field; +use Statamic\Fieldtypes\Entries as EntriesFieldtype; use Statamic\Fieldtypes\Integer as IntegerFieldtype; use Statamic\Fieldtypes\Terms as TermsFieldtype; use Statamic\Fieldtypes\Text; use Statamic\Query\Scopes\Filters\Fields\Dimensions; +use Tests\Factories\EntryFactory; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; @@ -90,4 +94,67 @@ public function it_shows_the_terms_filter_badge_in_the_selected_site() Site::setSelected('en'); $this->assertEquals('Tags: One', $filter->badge(['operator' => 'like', 'term' => 'one'])); } + + #[Test] + #[DataProvider('entriesFilterProvider')] + public function it_applies_the_entries_filter($maxItems, $values, $expected) + { + Collection::make('pages')->save(); + Collection::make('topics')->save(); + + (new EntryFactory)->collection('topics')->id('topic-1')->slug('topic-one')->data(['title' => 'Topic One'])->create(); + (new EntryFactory)->collection('topics')->id('topic-2')->slug('topic-two')->data(['title' => 'Topic Two'])->create(); + + if ($maxItems === 1) { + (new EntryFactory)->collection('pages')->id('page-a')->slug('page-a')->data(['related' => 'topic-1'])->create(); + (new EntryFactory)->collection('pages')->id('page-b')->slug('page-b')->data(['related' => 'topic-2'])->create(); + } else { + (new EntryFactory)->collection('pages')->id('page-a')->slug('page-a')->data(['related' => ['topic-1']])->create(); + (new EntryFactory)->collection('pages')->id('page-b')->slug('page-b')->data(['related' => ['topic-1', 'topic-2']])->create(); + } + + (new EntryFactory)->collection('pages')->id('page-c')->slug('page-c')->create(); + + $filter = (new EntriesFieldtype) + ->setField(new Field('related', ['type' => 'entries', 'max_items' => $maxItems])) + ->filter(); + + $query = Entry::query()->where('collection', 'pages'); + $filter->apply($query, 'related', $values); + + $this->assertEquals($expected, $query->get()->map->id()->sort()->values()->all()); + } + + public static function entriesFilterProvider() + { + return [ + 'single: is' => [1, ['operator' => '=', 'value' => 'topic-1'], ['page-a']], + 'single: isnt' => [1, ['operator' => '!=', 'value' => 'topic-1'], ['page-b', 'page-c']], + 'single: empty' => [1, ['operator' => 'null', 'value' => null], ['page-c']], + 'single: not empty' => [1, ['operator' => 'not-null', 'value' => null], ['page-a', 'page-b']], + 'single: no entry selected' => [1, ['operator' => '=', 'value' => null], ['page-a', 'page-b', 'page-c']], + 'multiple: is' => [null, ['operator' => '=', 'value' => 'topic-1'], ['page-a', 'page-b']], + 'multiple: is, only one match' => [null, ['operator' => '=', 'value' => 'topic-2'], ['page-b']], + 'multiple: isnt' => [null, ['operator' => '!=', 'value' => 'topic-1'], ['page-c']], + 'multiple: empty' => [null, ['operator' => 'null', 'value' => null], ['page-c']], + 'multiple: not empty' => [null, ['operator' => 'not-null', 'value' => null], ['page-a', 'page-b']], + ]; + } + + #[Test] + public function it_shows_the_entries_filter_badge() + { + Collection::make('topics')->save(); + + (new EntryFactory)->collection('topics')->id('topic-1')->slug('topic-one')->data(['title' => 'Topic One'])->create(); + + $filter = (new EntriesFieldtype) + ->setField(new Field('related', ['type' => 'entries', 'display' => 'Related'])) + ->filter(); + + $this->assertEquals('Related is Topic One', $filter->badge(['operator' => '=', 'value' => 'topic-1'])); + $this->assertEquals("Related isn't Topic One", $filter->badge(['operator' => '!=', 'value' => 'topic-1'])); + $this->assertEquals('Related empty', $filter->badge(['operator' => 'null', 'value' => null])); + $this->assertEquals('Related not empty', $filter->badge(['operator' => 'not-null', 'value' => null])); + } }