diff --git a/src/CP/Navigation/NavItem.php b/src/CP/Navigation/NavItem.php index 8135a640c93..37f029a180a 100644 --- a/src/CP/Navigation/NavItem.php +++ b/src/CP/Navigation/NavItem.php @@ -2,7 +2,9 @@ namespace Statamic\CP\Navigation; +use Illuminate\Support\Collection; use Statamic\Facades\CP\Nav; +use Statamic\Facades\URL; use Statamic\Statamic; use Statamic\Support\Html; use Statamic\Support\Str; @@ -131,6 +133,21 @@ public function url($url = null) ->value($url); } + /** + * Generate active URL pattern to determine when to resolve children for `hasActiveChild()` checks. + * + * @param string $url + * @return string + */ + protected function generateActivePatternForCpUrl($url) + { + $cpUrl = url(config('statamic.cp.route')).'/'; + + $relativeUrl = str_replace($cpUrl, '', URL::removeQueryAndFragment($url)); + + return $relativeUrl.'(/(.*)?|$)'; + } + /** * Get editable url for nav builder UI. */ @@ -239,6 +256,32 @@ public function isChild($isChild = null) ->value($isChild); } + /** + * Active URL pattern to determine when to resolve children for `hasActiveChild()` checks. + * + * @return $this + */ + public function active($pattern = null) + { + return $this->fluentlyGetOrSet('active')->value($pattern); + } + + /** + * Determine when to resolve children for `hasActiveChild()` checks. + * + * @return bool + */ + protected function shouldResolveChildren() + { + if (! $this->active) { + return false; + } + + $pattern = preg_quote(config('statamic.cp.route'), '#').'/'.$this->active; + + return preg_match('#'.$pattern.'#', request()->decodedPath()) === 1; + } + /** * Resolve children closure. * @@ -300,30 +343,38 @@ public function can($ability = null, $arguments = []) } /** - * Get or set pattern for active state styling. + * Get whether the nav item is currently active. * - * @param string|null $pattern - * @return mixed + * @return bool */ - public function active($pattern = null) + public function isActive() { - return $this->fluentlyGetOrSet('active')->value($pattern); + if ($this->hasActiveChild()) { + return true; + } + + return request()->url() === URL::removeQueryAndFragment($this->url); } /** - * Get whether the nav item is currently active. + * Get whether the nav item has a currently active child. * * @return bool */ - public function isActive() + protected function hasActiveChild() { - if (! $this->active) { - return false; + if ($this->shouldResolveChildren()) { + $this->resolveChildren(); } - $pattern = preg_quote(config('statamic.cp.route'), '#').'/'.$this->active; + if (! $this->children() instanceof Collection) { + return false; + } - return preg_match('#'.$pattern.'#', request()->decodedPath()) === 1; + return $this + ->children() + ->filter(fn ($item) => $item->isActive()) + ->isNotEmpty(); } /** @@ -441,22 +492,4 @@ public static function snakeCase($string) return $string; } - - /** - * Generate active pattern for CP url. - * - * @param string $url - * @return string - */ - protected function generateActivePatternForCpUrl($url) - { - $cpUrl = url(config('statamic.cp.route')).'/'; - - $url = Str::before($url, '?'); // Remove query params - $url = Str::before($url, '#'); // Remove anchors - - $relativeUrl = str_replace($cpUrl, '', $url); - - return $relativeUrl.'(/(.*)?|$)'; - } } diff --git a/src/Facades/Endpoint/URL.php b/src/Facades/Endpoint/URL.php index 9a3f6cc2267..cd4e1261b65 100644 --- a/src/Facades/Endpoint/URL.php +++ b/src/Facades/Endpoint/URL.php @@ -301,4 +301,18 @@ public function gravatar($email, $size = null) return $url; } + + /** + * Remove query and fragment from end of URL. + * + * @param string $url + * @return string + */ + public function removeQueryAndFragment($url) + { + $url = Str::before($url, '?'); // Remove query params + $url = Str::before($url, '#'); // Remove anchor fragment + + return $url; + } } diff --git a/tests/CP/Navigation/NavPreferencesTest.php b/tests/CP/Navigation/NavPreferencesTest.php index 0257d3d8ef1..5b3b64510e1 100644 --- a/tests/CP/Navigation/NavPreferencesTest.php +++ b/tests/CP/Navigation/NavPreferencesTest.php @@ -2,6 +2,7 @@ namespace Tests\CP\Navigation; +use Illuminate\Support\Facades\Request; use Statamic\Facades; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; @@ -1637,6 +1638,55 @@ public function it_can_build_with_hidden_items() $this->assertEquals('@modify', $contentItems->keyBy->display()->get('Globetrotters')->manipulations()['action']); } + /** @test */ + public function it_checks_active_status_on_moved_items() + { + Facades\Taxonomy::make('topics')->save(); + Facades\Taxonomy::make('tags')->save(); + + $items = $this->buildNavWithPreferences([ + 'top_level' => [ + 'content::collections::articles' => [ + 'action' => '@move', + 'children' => [ + 'content::taxonomies::topics' => '@move', + ], + ], + ], + ], true); + + $articles = $items->get('Top Level')->keyBy->display()->get('Articles'); + $topics = $articles->children()->first(); + $taxonomies = $items->get('Content')->keyBy->display()->get('Taxonomies'); + $tags = $items->get('Content')->keyBy->display()->get('Taxonomies')->children()->first(); + + $this->assertFalse($articles->isActive()); + $this->assertFalse($topics->isActive()); + $this->assertFalse($taxonomies->isActive()); + $this->assertFalse($tags->isActive()); + + Request::swap(Request::create('http://localhost/cp/collections/articles')); + + $this->assertTrue($articles->isActive()); + $this->assertFalse($topics->isActive()); + $this->assertFalse($taxonomies->isActive()); + $this->assertFalse($tags->isActive()); + + Request::swap(Request::create('http://localhost/cp/taxonomies/topics')); + + $this->assertTrue($articles->isActive()); + $this->assertTrue($topics->isActive()); + $this->assertFalse($taxonomies->isActive()); + $this->assertFalse($tags->isActive()); + + Request::swap(Request::create('http://localhost/cp/taxonomies/tags')); + + $this->assertFalse($articles->isActive()); + $this->assertFalse($topics->isActive()); + $this->assertTrue($taxonomies->isActive()); + $this->assertTrue($tags->isActive()); + } + private function buildNavWithPreferences($preferences, $withHidden = false) { $this->actingAs(tap(Facades\User::make()->makeSuper())->save()); diff --git a/tests/CP/Navigation/NavTest.php b/tests/CP/Navigation/NavTest.php index 0636f4d3f8e..0fe2d54bde4 100644 --- a/tests/CP/Navigation/NavTest.php +++ b/tests/CP/Navigation/NavTest.php @@ -433,15 +433,15 @@ public function it_checks_if_active() $this->assertFalse($hello->isActive()); $this->assertFalse($helloWithQueryParams->isActive()); $this->assertFalse($helloWithAnchor->isActive()); - $this->assertTrue($hell->isActive()); + $this->assertFalse($hell->isActive()); $this->assertFalse($localNotCp->isActive()); $this->assertFalse($external->isActive()); $this->assertFalse($externalSecure->isActive()); Request::swap(Request::create('http://localhost/cp/hello/test')); - $this->assertTrue($hello->isActive()); - $this->assertTrue($helloWithQueryParams->isActive()); - $this->assertTrue($helloWithAnchor->isActive()); + $this->assertFalse($hello->isActive()); + $this->assertFalse($helloWithQueryParams->isActive()); + $this->assertFalse($helloWithAnchor->isActive()); $this->assertFalse($hell->isActive()); $this->assertFalse($localNotCp->isActive()); $this->assertFalse($external->isActive()); @@ -466,6 +466,66 @@ public function it_checks_if_active() $this->assertFalse($externalSecure->isActive()); } + /** @test */ + public function it_checks_if_has_active_children() + { + $collections = Nav::content('Collections') + ->url('http://localhost/cp/collections') + ->children(function () use (&$pages, &$articles) { + return [ + $pages = Nav::item('Pages')->url('/cp/collections/pages'), + $articles = Nav::item('Articles')->url('/cp/collections/articles'), + ]; + }); + + Request::swap(Request::create('http://localhost/cp/collections/articles')); + $this->assertTrue($collections->isActive()); + $this->assertFalse($pages->isActive()); + $this->assertTrue($articles->isActive()); + } + + /** @test */ + public function it_can_get_has_active_children_status_with_custom_resolve_children_pattern() + { + $collections = Nav::content('Custom Collections Url') + ->url('http://localhost/cp/custom/url') + ->active('collections*') + ->children(function () use (&$pages, &$articles) { + return [ + $pages = Nav::item('Pages')->url('/cp/collections/pages'), + $articles = Nav::item('Articles')->url('/cp/collections/articles'), + ]; + }); + + Request::swap(Request::create('http://localhost/cp/collections/articles')); + $this->assertTrue($collections->isActive()); + $this->assertFalse($pages->isActive()); + $this->assertTrue($articles->isActive()); + } + + /** + * @deprecated + * + * @test + */ + public function it_can_get_has_active_children_status_with_deprecated_active_pattern() + { + $collections = Nav::content('Custom Collections Url') + ->url('http://localhost/cp/custom/url') + ->active('collections*') + ->children(function () use (&$pages, &$articles) { + return [ + $pages = Nav::item('Pages')->url('/cp/collections/pages'), + $articles = Nav::item('Articles')->url('/cp/collections/articles'), + ]; + }); + + Request::swap(Request::create('http://localhost/cp/collections/articles')); + $this->assertTrue($collections->isActive()); + $this->assertFalse($pages->isActive()); + $this->assertTrue($articles->isActive()); + } + /** @test */ public function it_sets_the_url() { @@ -502,7 +562,7 @@ public function it_gets_a_cleaner_editable_version_of_the_url() } /** @test */ - public function it_does_not_automatically_add_an_active_pattern_when_setting_url_if_one_is_already_defined() + public function it_does_not_automatically_add_a_resolve_children_pattern_when_setting_url_if_one_is_already_defined() { $nav = Nav::create('cp-relative')->active('foo.*')->url('foo/bar'); $this->assertEquals('http://localhost/cp/foo/bar', $nav->url()); diff --git a/tests/Facades/UrlTest.php b/tests/Facades/UrlTest.php index 8cc5673540e..74c9e534ab0 100644 --- a/tests/Facades/UrlTest.php +++ b/tests/Facades/UrlTest.php @@ -229,4 +229,23 @@ public function relativeProvider() ['/foo/bar/?bar=baz#fragment', '/foo/bar/?bar=baz#fragment'], ]; } + + /** @test */ + public function it_can_remove_query_and_fragment() + { + $this->assertEquals('https://example.com', URL::removeQueryAndFragment('https://example.com?query')); + $this->assertEquals('https://example.com', URL::removeQueryAndFragment('https://example.com#anchor')); + $this->assertEquals('https://example.com', URL::removeQueryAndFragment('https://example.com?foo=bar&baz=qux')); + $this->assertEquals('https://example.com', URL::removeQueryAndFragment('https://example.com?foo=bar&baz=qux#anchor')); + + $this->assertEquals('https://example.com/', URL::removeQueryAndFragment('https://example.com/?query')); + $this->assertEquals('https://example.com/', URL::removeQueryAndFragment('https://example.com/#anchor')); + $this->assertEquals('https://example.com/', URL::removeQueryAndFragment('https://example.com/?foo=bar&baz=qux')); + $this->assertEquals('https://example.com/', URL::removeQueryAndFragment('https://example.com/?foo=bar&baz=qux#anchor')); + + $this->assertEquals('https://example.com/about', URL::removeQueryAndFragment('https://example.com/about?query')); + $this->assertEquals('https://example.com/about', URL::removeQueryAndFragment('https://example.com/about#anchor')); + $this->assertEquals('https://example.com/about', URL::removeQueryAndFragment('https://example.com/about?foo=bar&baz=qux')); + $this->assertEquals('https://example.com/about', URL::removeQueryAndFragment('https://example.com/about?foo=bar&baz=qux#anchor')); + } }