diff --git a/src/Tags/Collection/Entries.php b/src/Tags/Collection/Entries.php index d67b7abe39..27699513b2 100644 --- a/src/Tags/Collection/Entries.php +++ b/src/Tags/Collection/Entries.php @@ -5,12 +5,14 @@ use Closure; use Illuminate\Support\Carbon; use Illuminate\Support\Collection as IlluminateCollection; +use Statamic\Contracts\Entries\QueryBuilder; use Statamic\Contracts\Taxonomies\Term; use Statamic\Entries\EntryCollection; use Statamic\Facades\Collection; use Statamic\Facades\Compare; use Statamic\Facades\Entry; use Statamic\Facades\Site; +use Statamic\Query\OrderBy; use Statamic\Support\Arr; use Statamic\Support\Str; use Statamic\Tags\Concerns; @@ -66,18 +68,22 @@ public function next($currentEntry) throw_if($this->params->has('offset'), new \Exception('collection:next is not compatible with [offset] parameter')); throw_if($this->collections->count() > 1, new \Exception('collection:next is not compatible with multiple collections')); + if ($this->orderBys->count() === 1) { + $this->orderBys[] = new OrderBy('title', 'asc'); + } + $collection = $this->collections->first(); $primaryOrderBy = $this->orderBys->first(); + $secondaryOrderBy = $this->orderBys->get(1); - if ($primaryOrderBy->direction === 'desc') { - $operator = '<'; - } + $primaryOperator = $primaryOrderBy->direction === 'desc' ? '<' : '>'; + $secondaryOperator = $secondaryOrderBy->direction === 'desc' ? '<' : '>'; if ($primaryOrderBy->sort === 'order') { throw_if(! $currentOrder = $currentEntry->order(), new \Exception('Current entry does not have an order')); - $query = $this->query()->where('order', $operator ?? '>', $currentOrder); + $query = $this->query()->where('order', $primaryOperator, $currentOrder); } elseif ($collection->dated() && $primaryOrderBy->sort === 'date') { - $query = $this->query()->where('date', $operator ?? '>', $currentEntry->date()); + $query = $this->queryPreviousNextByDate($currentEntry, $primaryOperator, $secondaryOperator); } else { throw new \Exception('collection:next requires ordered or dated collection'); } @@ -91,18 +97,22 @@ public function previous($currentEntry) throw_if($this->params->has('offset'), new \Exception('collection:previous is not compatible with [offset] parameter')); throw_if($this->collections->count() > 1, new \Exception('collection:previous is not compatible with multiple collections')); + if ($this->orderBys->count() === 1) { + $this->orderBys[] = new OrderBy('title', 'asc'); + } + $collection = $this->collections->first(); $primaryOrderBy = $this->orderBys->first(); + $secondaryOrderBy = $this->orderBys->get(1); - if ($primaryOrderBy->direction === 'desc') { - $operator = '>'; - } + $primaryOperator = $primaryOrderBy->direction === 'desc' ? '>' : '<'; + $secondaryOperator = $secondaryOrderBy->direction === 'desc' ? '>' : '<'; if ($primaryOrderBy->sort === 'order') { throw_if(! $currentOrder = $currentEntry->order(), new \Exception('Current entry does not have an order')); - $query = $this->query()->where('order', $operator ?? '<', $currentOrder); + $query = $this->query()->where('order', $primaryOperator, $currentOrder); } elseif ($collection->dated() && $primaryOrderBy->sort === 'date') { - $query = $this->query()->where('date', $operator ?? '<', $currentEntry->date()); + $query = $this->queryPreviousNextByDate($currentEntry, $primaryOperator, $secondaryOperator); } else { throw new \Exception('collection:previous requires ordered or dated collection'); } @@ -141,6 +151,32 @@ public function newer($currentEntry) : $this->previous($currentEntry); } + protected function queryPreviousNextByDate($currentEntry, string $primaryOperator, string $secondaryOperator): QueryBuilder + { + $primaryOrderBy = $this->orderBys->first(); + $secondaryOrderBy = $this->orderBys->get(1); + + $currentEntryDate = $currentEntry->date(); + + // Get the IDs of any items that have the same date as the current entry, + // but come before/after the current entry sorted by the second column. + $previousOfSame = $this->query() + ->where('date', $currentEntryDate) + ->orderBy($secondaryOrderBy->sort, $secondaryOrderBy->direction) + ->where($secondaryOrderBy->sort, $secondaryOperator, $currentEntry->value($secondaryOrderBy->sort)) + ->get() + ->pluck('id') + ->toArray(); + + return $this->query() + ->where(fn ($query) => $query + ->where('date', $primaryOperator, $currentEntryDate) + ->orWhereIn('id', $previousOfSame) + ) + ->orderBy('date', $primaryOrderBy->direction) + ->orderBy($secondaryOrderBy->sort, $secondaryOrderBy->direction); + } + protected function query() { $query = Entry::query() diff --git a/tests/Tags/Collection/CollectionTest.php b/tests/Tags/Collection/CollectionTest.php index 02ee17724a..c97447995a 100644 --- a/tests/Tags/Collection/CollectionTest.php +++ b/tests/Tags/Collection/CollectionTest.php @@ -342,6 +342,32 @@ public function it_can_get_previous_and_next_entries_in_a_dated_desc_collection( $this->assertEquals(['Grape', 'Hummus', 'Fig'], $this->runTagAndGetTitles('newer')); // Alias of prev when date:desc } + /** + * @test + * https://github.com/statamic/cms/issues/1831 + */ + public function it_can_get_previous_and_next_entries_in_a_dated_desc_collection_when_multiple_entries_share_the_same_date() + { + $this->foods->dated(true)->save(); + + $this->makeEntry($this->foods, 'a')->date('2023-01-01')->set('title', 'Apple')->save(); + $this->makeEntry($this->foods, 'b')->date('2023-02-05')->set('title', 'Banana')->save(); + $this->makeEntry($this->foods, 'c')->date('2023-02-05')->set('title', 'Carrot')->save(); + $this->makeEntry($this->foods, 'd')->date('2023-03-07')->set('title', 'Danish')->save(); + + $this->setTagParameters([ + 'in' => 'foods', + 'current' => $this->findEntryByTitle('Carrot')->id(), + 'order_by' => 'date:desc|title:desc', + 'limit' => 1, + ]); + + $this->assertEquals(['Danish'], $this->runTagAndGetTitles('previous')); + $this->assertEquals(['Danish'], $this->runTagAndGetTitles('newer')); // Alias of prev when date:desc + $this->assertEquals(['Banana'], $this->runTagAndGetTitles('next')); + $this->assertEquals(['Banana'], $this->runTagAndGetTitles('older')); // Alias of next when date:desc + } + /** @test */ public function it_can_get_previous_and_next_entries_in_a_dated_asc_collection() { @@ -384,6 +410,32 @@ public function it_can_get_previous_and_next_entries_in_a_dated_asc_collection() $this->assertEquals(['Carrot', 'Banana', 'Danish'], $this->runTagAndGetTitles('older')); // Alias of prev when date:desc } + /** + * @test + * https://github.com/statamic/cms/issues/1831 + */ + public function it_can_get_previous_and_next_entries_in_a_dated_asc_collection_when_multiple_entries_share_the_same_date() + { + $this->foods->dated(true)->save(); + + $this->makeEntry($this->foods, 'a')->date('2023-01-01')->set('title', 'Apple')->save(); + $this->makeEntry($this->foods, 'b')->date('2023-02-05')->set('title', 'Banana')->save(); + $this->makeEntry($this->foods, 'c')->date('2023-02-05')->set('title', 'Carrot')->save(); + $this->makeEntry($this->foods, 'd')->date('2023-03-07')->set('title', 'Danish')->save(); + + $this->setTagParameters([ + 'in' => 'foods', + 'current' => $this->findEntryByTitle('Carrot')->id(), + 'order_by' => 'date:asc|title:asc', + 'limit' => 1, + ]); + + $this->assertEquals(['Banana'], $this->runTagAndGetTitles('previous')); + $this->assertEquals(['Banana'], $this->runTagAndGetTitles('older')); // Alias of previous when date:desc + $this->assertEquals(['Danish'], $this->runTagAndGetTitles('next')); + $this->assertEquals(['Danish'], $this->runTagAndGetTitles('newer')); // Alias of next when date:asc + } + /** @test */ public function it_can_get_previous_and_next_entries_in_an_orderable_asc_collection() {