diff --git a/src/CP/Navigation/Nav.php b/src/CP/Navigation/Nav.php index d8908393489..27b4ebee964 100644 --- a/src/CP/Navigation/Nav.php +++ b/src/CP/Navigation/Nav.php @@ -143,6 +143,14 @@ public function buildWithoutPreferences($withHidden = false) return $this->build(false, $withHidden); } + /** + * Clear cached urls. + */ + public function clearCachedUrls() + { + return NavBuilder::clearCachedUrls(); + } + /** * Make base items. * diff --git a/src/CP/Navigation/NavBuilder.php b/src/CP/Navigation/NavBuilder.php index 659114ac7d8..4219e56c2e5 100644 --- a/src/CP/Navigation/NavBuilder.php +++ b/src/CP/Navigation/NavBuilder.php @@ -3,6 +3,8 @@ namespace Statamic\CP\Navigation; use Exception; +use Illuminate\Support\Facades\Cache; +use Statamic\Facades\Blink; use Statamic\Facades\Preference; use Statamic\Facades\User; use Statamic\Support\Arr; @@ -10,14 +12,20 @@ class NavBuilder { + const UNRESOLVED_CHILDREN_URLS_CACHE_KEY = 'cp-nav-urls-unresolved-children'; + const ALL_URLS_CACHE_KEY = 'cp-nav-urls-all'; + protected $items = []; protected $pendingItems = []; protected $withHidden = false; + protected $itemsWithChildrenClosures = []; protected $sections = []; protected $sectionsOriginalItemIds = []; protected $sectionsManipulations = []; protected $sectionsOrder = []; protected $sectionsWithReorderedItems = []; + protected $urlsUnresolvedChildren = []; + protected $urlsAll = []; protected $built; /** @@ -45,7 +53,8 @@ public function build($preferences = true) } return $this - ->buildChildren() + ->trackChildrenClosures() + ->resolveChildrenClosures() ->validateNesting() ->validateViews() ->authorizeItems() @@ -53,17 +62,33 @@ public function build($preferences = true) ->syncOriginal() ->trackCoreSections() ->trackOriginalSectionItems() + ->trackUrls() ->applyPreferenceOverrides($preferences) ->buildSections() + ->blinkUrls() ->get(); } /** - * Build children closures. + * Track children closures. + * + * @return $this + */ + protected function trackChildrenClosures() + { + collect($this->items) + ->filter(fn ($item) => is_callable($item->children())) + ->each(fn ($item) => $this->itemsWithChildrenClosures[] = $item->id()); + + return $this; + } + + /** + * Resolve children closures. * * @return $this */ - protected function buildChildren() + protected function resolveChildrenClosures() { collect($this->items) ->filter(fn ($item) => $item->isActive() || $this->withHidden) @@ -805,6 +830,12 @@ protected function userRemoveItemFromChildren($item) return; } + if ($this->urlsUnresolvedChildren->has($parent->id())) { + $this->urlsUnresolvedChildren[$parent->id()] = collect($this->urlsUnresolvedChildren[$parent->id()]) + ->reject(fn ($url) => $url === $item->url()) + ->all(); + } + if ($parent->resolveChildren()->children()) { $parent->children( $parent->children()->reject(function ($child) use ($item) { @@ -884,6 +915,139 @@ protected function generateNewItemId($section, $name) return (new NavItem)->display($name)->section($section)->id(); } + /** + * Track URLs for `isActive` checks on nav items. + * + * @return $this + */ + protected function trackUrls() + { + // If URLs are already cached, get them from cache so that we don't have to + // resolve children closures on every request for performance reasons. + if ($this->hasCachedUrls()) { + $this->urlsUnresolvedChildren = Cache::get(static::UNRESOLVED_CHILDREN_URLS_CACHE_KEY); + $this->urlsAll = Cache::get(static::ALL_URLS_CACHE_KEY); + $this->ensureUrlCachesAreUpToDate(); + + return $this; + } + + $this->urlsUnresolvedChildren = collect($this->items) + ->filter(fn ($item) => collect($this->itemsWithChildrenClosures)->contains($item->id())) + ->mapWithKeys(function ($item) { + return [$item->id() => $item->resolveChildren()->children()?->map->url()->all() ?? []]; + }); + + $this->urlsAll = collect($this->items) + ->flatMap(function ($item) { + return array_merge([$item->url()], $item->resolveChildren()->children()?->map->url()->all() ?? []); + }) + ->unique() + ->values(); + + $this->cacheUrls(); + + return $this; + } + + /** + * Cache tracked URLs. + */ + protected function cacheUrls() + { + Cache::put(static::UNRESOLVED_CHILDREN_URLS_CACHE_KEY, $this->urlsUnresolvedChildren); + Cache::put(static::ALL_URLS_CACHE_KEY, $this->urlsAll); + } + + /** + * Ensure URL caches are up to date. + */ + protected function ensureUrlCachesAreUpToDate() + { + $updated = collect($this->items) + ->filter(fn ($item) => collect($this->itemsWithChildrenClosures)->contains($item->id())) + ->filter(fn ($item) => $item->isActive() || $this->withHidden) + ->mapWithKeys(fn ($item) => [$item->id() => $item->children()?->map->url()->all() ?? []]) + ->filter(fn ($urls, $id) => $this->urlsUnresolvedChildren->get($id) != $urls) + ->each(fn ($urls, $id) => $this->trackChangedChildren($id, $urls)) + ->isNotEmpty(); + + if ($updated) { + $this->cacheUrls(); + } + } + + /** + * Track changed children URLs. + */ + protected function trackChangedChildren($id, $urls) + { + $this->urlsUnresolvedChildren->put($id, $urls); + + $this->urlsAll = $this->urlsAll + ->merge($urls) + ->unique() + ->values(); + } + + /** + * Check if cache has URLs. + * + * @return bool + */ + protected function hasCachedUrls() + { + return Cache::has(static::UNRESOLVED_CHILDREN_URLS_CACHE_KEY) + && Cache::has(static::ALL_URLS_CACHE_KEY); + } + + /** + * Blink URLs for `isActive` checks during this request. + * + * @return $this + */ + protected function blinkUrls() + { + Blink::put(static::UNRESOLVED_CHILDREN_URLS_CACHE_KEY, $this->urlsUnresolvedChildren); + Blink::put(static::ALL_URLS_CACHE_KEY, $this->urlsAll); + + return $this; + } + + /** + * Get unresolved children URLs for an item's `isActive` checks. + * + * @return \Illuminate\Support\Collection + */ + public static function getUnresolvedChildrenUrlsForItem($item) + { + return Blink::get(static::UNRESOLVED_CHILDREN_URLS_CACHE_KEY)?->get($item->id()) + ?? Cache::get(static::UNRESOLVED_CHILDREN_URLS_CACHE_KEY)?->get($item->id()); + } + + /** + * Get all URLs explicitly used in nav for `isActive` checks. + * + * @return \Illuminate\Support\Collection + */ + public static function getAllUrls() + { + return Blink::get(static::ALL_URLS_CACHE_KEY) + ?? Cache::get(static::ALL_URLS_CACHE_KEY) + ?? collect(); + } + + /** + * Clear cached urls. Important when saving/deleting CP nav preferences, etc. + */ + public static function clearCachedUrls() + { + Cache::forget(static::UNRESOLVED_CHILDREN_URLS_CACHE_KEY); + Blink::forget(static::UNRESOLVED_CHILDREN_URLS_CACHE_KEY); + Cache::forget(static::ALL_URLS_CACHE_KEY); + Blink::forget(static::ALL_URLS_CACHE_KEY); + } + /** * Get built nav. * diff --git a/src/CP/Navigation/NavItem.php b/src/CP/Navigation/NavItem.php index 37f029a180a..ec931f7b9c8 100644 --- a/src/CP/Navigation/NavItem.php +++ b/src/CP/Navigation/NavItem.php @@ -21,6 +21,7 @@ class NavItem protected $icon; protected $children; protected $isChild; + protected $wasOriginallyChild; protected $authorization; protected $active; protected $view; @@ -148,6 +149,21 @@ protected function generateActivePatternForCpUrl($url) return $relativeUrl.'(/(.*)?|$)'; } + /** + * Generate active URL patterns for this item's children. + * + * @return Collection + */ + protected function generateActivePatternsForChildren() + { + if (! $this->children()) { + return collect(); + } + + return collect(NavBuilder::getUnresolvedChildrenUrlsForItem($this) ?? []) + ->map(fn ($url) => $this->generateActivePatternForCpUrl($url)); + } + /** * Get editable url for nav builder UI. */ @@ -253,33 +269,39 @@ public function isChild($isChild = null) ->getter(function ($value) { return (bool) $value; }) + ->afterSetter(function ($value) { + if ($value === true && ! isset($this->wasOriginallyChild)) { + $this->wasOriginallyChild = $value; + } + }) ->value($isChild); } /** - * Active URL pattern to determine when to resolve children for `hasActiveChild()` checks. + * Check if this nav item was ever a child before user preferences were applied. * - * @return $this + * @param bool|null $isChild + * @return mixed */ - public function active($pattern = null) + protected function wasOriginallyChild() { - return $this->fluentlyGetOrSet('active')->value($pattern); + return (bool) $this->wasOriginallyChild; } /** - * Determine when to resolve children for `hasActiveChild()` checks. + * Active URL pattern to determine when to resolve children for `hasActiveChild()` checks. * - * @return bool + * Though we still check active patterns for nested URLs internally, having to manually + * use this method should not be needed anymore, not to mention it is confusing for + * addon devs to know when they even need to use it, thus we are deprecating it. + * + * @deprecated + * + * @return $this */ - protected function shouldResolveChildren() + public function active($pattern = null) { - if (! $this->active) { - return false; - } - - $pattern = preg_quote(config('statamic.cp.route'), '#').'/'.$this->active; - - return preg_match('#'.$pattern.'#', request()->decodedPath()) === 1; + return $this->fluentlyGetOrSet('active')->value($pattern); } /** @@ -343,7 +365,7 @@ public function can($ability = null, $arguments = []) } /** - * Get whether the nav item is currently active. + * Determine whether the nav item is currently active. * * @return bool */ @@ -353,27 +375,73 @@ public function isActive() return true; } + // If the current URL is not explicitly referenced in the CP nav, + // and if this item is/was ever a child nav item, + // then check against URL heirarchy conventions using regex pattern. + if ($this->currentUrlIsNotExplicitlyReferencedInNav() && $this->wasOriginallyChild()) { + return $this->isActiveByPattern($this->active); + } + return request()->url() === URL::removeQueryAndFragment($this->url); } /** - * Get whether the nav item has a currently active child. + * Determine whether the nav item has a currently active child. * * @return bool */ protected function hasActiveChild() { - if ($this->shouldResolveChildren()) { - $this->resolveChildren(); + // If children are already resolved to a collection, just check `isActive()` on each child item. + if ($this->children() instanceof Collection) { + return $this + ->children() + ->filter(fn ($item) => $item->isActive()) + ->isNotEmpty(); + } + + // If the current URL is not explicitly referenced in the CP nav, + // and if this item has children to check against, + // then check against URL heirarchy conventions using regex pattern. + if ($this->currentUrlIsNotExplicitlyReferencedInNav() && $this->children()) { + return $this->isActiveByPattern($this->generateActivePatternsForChildren()); + } + + // If children closure has not been resolved, and children urls are cached, check against cached children. + if ($childrenUrls = NavBuilder::getUnresolvedChildrenUrlsForItem($this)) { + return collect($childrenUrls) + ->map(fn ($url) => URL::removeQueryAndFragment($url)) + ->contains(request()->url()); } - if (! $this->children() instanceof Collection) { + return false; + } + + /** + * Determine whether the current URL is explicitly referenced in nav. + * + * @return bool + */ + protected function currentUrlIsNotExplicitlyReferencedInNav() + { + return ! NavBuilder::getAllUrls()->contains(request()->url()); + } + + /** + * Determine whether the nav item is currently active using the regex technique for deeply nested hierarchical urls. + * + * @param string|array $active + * @return bool + */ + protected function isActiveByPattern($active) + { + if (! $active) { return false; } - return $this - ->children() - ->filter(fn ($item) => $item->isActive()) + return collect($active) + ->map(fn ($pattern) => preg_quote(config('statamic.cp.route'), '#').'/'.$pattern) + ->filter(fn ($pattern) => preg_match('#'.$pattern.'#', request()->decodedPath()) === 1) ->isNotEmpty(); } diff --git a/src/Http/Controllers/CP/Preferences/Nav/DefaultNavController.php b/src/Http/Controllers/CP/Preferences/Nav/DefaultNavController.php index f71e1b2d05a..b0fcedc41af 100644 --- a/src/Http/Controllers/CP/Preferences/Nav/DefaultNavController.php +++ b/src/Http/Controllers/CP/Preferences/Nav/DefaultNavController.php @@ -41,6 +41,8 @@ public function update(Request $request) Preference::default()->set('nav', $nav)->save(); + Nav::clearCachedUrls(); + $this->success(__('Saved')); return true; @@ -50,6 +52,8 @@ public function destroy() { Preference::default()->remove('nav')->save(); + Nav::clearCachedUrls(); + return true; } } diff --git a/src/Http/Controllers/CP/Preferences/Nav/RoleNavController.php b/src/Http/Controllers/CP/Preferences/Nav/RoleNavController.php index 14bb65f0174..ced54f5354d 100644 --- a/src/Http/Controllers/CP/Preferences/Nav/RoleNavController.php +++ b/src/Http/Controllers/CP/Preferences/Nav/RoleNavController.php @@ -46,6 +46,8 @@ public function update(Request $request, $handle) $role->setPreference('nav', $nav)->save(); + Nav::clearCachedUrls(); + $this->success(__('Saved')); return true; @@ -57,6 +59,8 @@ public function destroy($handle) $role->removePreference('nav')->save(); + Nav::clearCachedUrls(); + return true; } } diff --git a/src/Http/Controllers/CP/Preferences/Nav/UserNavController.php b/src/Http/Controllers/CP/Preferences/Nav/UserNavController.php index a55c9319d33..6adbee28480 100644 --- a/src/Http/Controllers/CP/Preferences/Nav/UserNavController.php +++ b/src/Http/Controllers/CP/Preferences/Nav/UserNavController.php @@ -3,6 +3,7 @@ namespace Statamic\Http\Controllers\CP\Preferences\Nav; use Illuminate\Http\Request; +use Statamic\Facades\CP\Nav; use Statamic\Facades\User; use Statamic\Http\Controllers\Controller; @@ -26,6 +27,8 @@ public function update(Request $request) User::current()->setPreference('nav', $nav)->save(); + Nav::clearCachedUrls(); + $this->success(__('Saved')); return true; @@ -35,6 +38,8 @@ public function destroy() { User::current()->removePreference('nav')->save(); + Nav::clearCachedUrls(); + return true; } } diff --git a/tests/CP/Navigation/ActiveNavItemTest.php b/tests/CP/Navigation/ActiveNavItemTest.php new file mode 100644 index 00000000000..ce26c7fc03a --- /dev/null +++ b/tests/CP/Navigation/ActiveNavItemTest.php @@ -0,0 +1,861 @@ +actingAs(tap(User::make()->makeSuper())->save()); + + // TODO: Other tests are leaving behind forms without titles that are causing failures here? + Facades\Form::shouldReceive('all')->andReturn(collect()); + } + + protected function resolveApplicationConfiguration($app) + { + parent::resolveApplicationConfiguration($app); + + // Set up test routes for fake SEO Pro extension + $app->booted(function () { + Route::get('cp/seo-pro', fn () => 'test'); + Route::get('cp/seo-pro/section-defaults', fn () => 'test'); + Route::get('cp/seo-pro/section-defaults/pages', fn () => 'test'); + Route::get('cp/seo-pro/section-defaults/articles', fn () => 'test'); + Route::get('cp/totally-custom-url', fn () => 'test'); + Route::get('cp/totally-custom-url/deeper/descendant', fn () => 'test'); + }); + } + + /** @test */ + public function it_resolves_all_children_only_once_to_build_caches_for_is_active_checks() + { + Facades\Collection::make('pages')->title('Pages')->save(); + Facades\Collection::make('articles')->title('Articles')->save(); + + Facades\Taxonomy::make('tags')->title('Tags')->save(); + Facades\Taxonomy::make('categories')->title('Categories')->save(); + + // Clear caches + Nav::clearCachedUrls(); + $this->assertFalse(Cache::has(NavBuilder::UNRESOLVED_CHILDREN_URLS_CACHE_KEY)); + $this->assertFalse(Blink::has(NavBuilder::UNRESOLVED_CHILDREN_URLS_CACHE_KEY)); + $this->assertFalse(Cache::has(NavBuilder::ALL_URLS_CACHE_KEY)); + $this->assertFalse(Blink::has(NavBuilder::ALL_URLS_CACHE_KEY)); + + // Ensure that all children are resolved and URLs are cached for `isActive()` checks on first build + $nav = Nav::build()->pluck('items', 'display'); + $this->assertTrue(Cache::has(NavBuilder::UNRESOLVED_CHILDREN_URLS_CACHE_KEY)); + $this->assertTrue(Blink::has(NavBuilder::UNRESOLVED_CHILDREN_URLS_CACHE_KEY)); + $this->assertTrue(Cache::has(NavBuilder::ALL_URLS_CACHE_KEY)); + $this->assertTrue(Blink::has(NavBuilder::ALL_URLS_CACHE_KEY)); + $this->assertInstanceOf(Collection::class, $this->getItemByDisplay($nav->get('Content'), 'Collections')->children()); + $this->assertInstanceOf(Collection::class, $this->getItemByDisplay($nav->get('Content'), 'Taxonomies')->children()); + + // Ensure that it builds children as unresolved closures on second build + $nav = Nav::build()->pluck('items', 'display'); + $this->assertTrue(Cache::has(NavBuilder::UNRESOLVED_CHILDREN_URLS_CACHE_KEY)); + $this->assertTrue(Blink::has(NavBuilder::UNRESOLVED_CHILDREN_URLS_CACHE_KEY)); + $this->assertTrue(Cache::has(NavBuilder::ALL_URLS_CACHE_KEY)); + $this->assertTrue(Blink::has(NavBuilder::ALL_URLS_CACHE_KEY)); + $this->assertInstanceOf(Closure::class, $this->getItemByDisplay($nav->get('Content'), 'Collections')->children()); + $this->assertInstanceOf(Closure::class, $this->getItemByDisplay($nav->get('Content'), 'Taxonomies')->children()); + } + + /** @test */ + public function it_updates_caches_when_new_child_urls_are_detected_after_resolving_children() + { + Facades\Collection::make('pages')->title('Pages')->save(); + Facades\Collection::make('articles')->title('Articles')->save(); + + // Ensure we clear cached URLs and build nav cache + Nav::clearCachedUrls(); + Nav::build(); + + // Assert that our collection children are properly cached + $collectionsChildrenUrls = [ + 'http://localhost/cp/collections/articles', + 'http://localhost/cp/collections/pages', + ]; + $this->assertEquals($collectionsChildrenUrls, Cache::get(NavBuilder::UNRESOLVED_CHILDREN_URLS_CACHE_KEY)->get('content::collections')); + $this->assertEquals($collectionsChildrenUrls, Blink::get(NavBuilder::UNRESOLVED_CHILDREN_URLS_CACHE_KEY)->get('content::collections')); + collect($collectionsChildrenUrls)->each(function ($url) { + $this->assertTrue(Cache::get(NavBuilder::ALL_URLS_CACHE_KEY)->contains($url)); + $this->assertTrue(Blink::get(NavBuilder::ALL_URLS_CACHE_KEY)->contains($url)); + }); + + // Now let's create a new collection + Facades\Collection::make('products')->title('Products')->save(); + + // Simply building the nav should change what is cached + $collectionsChildrenUrls = [ + 'http://localhost/cp/collections/articles', + 'http://localhost/cp/collections/pages', + ]; + $this->assertEquals($collectionsChildrenUrls, Cache::get(NavBuilder::UNRESOLVED_CHILDREN_URLS_CACHE_KEY)->get('content::collections')); + $this->assertEquals($collectionsChildrenUrls, Blink::get(NavBuilder::UNRESOLVED_CHILDREN_URLS_CACHE_KEY)->get('content::collections')); + collect($collectionsChildrenUrls)->each(function ($url) { + $this->assertTrue(Cache::get(NavBuilder::ALL_URLS_CACHE_KEY)->contains($url)); + $this->assertTrue(Blink::get(NavBuilder::ALL_URLS_CACHE_KEY)->contains($url)); + }); + + // But if we build the nav again by hitting collections url to resolve its' children, the caches should get updated + $this + ->get('http://localhost/cp/collections') + ->assertStatus(200); + + // Assert that our collection children caches are properly updated + $updatedChildrenUrls = [ + 'http://localhost/cp/collections/articles', + 'http://localhost/cp/collections/pages', + 'http://localhost/cp/collections/products', + ]; + $this->assertEquals($updatedChildrenUrls, Cache::get(NavBuilder::UNRESOLVED_CHILDREN_URLS_CACHE_KEY)->get('content::collections')); + $this->assertEquals($updatedChildrenUrls, Blink::get(NavBuilder::UNRESOLVED_CHILDREN_URLS_CACHE_KEY)->get('content::collections')); + collect($updatedChildrenUrls)->each(function ($url) { + $this->assertTrue(Cache::get(NavBuilder::ALL_URLS_CACHE_KEY)->contains($url)); + $this->assertTrue(Blink::get(NavBuilder::ALL_URLS_CACHE_KEY)->contains($url)); + }); + } + + /** @test */ + public function it_builds_core_children_closure_when_not_active() + { + Facades\Collection::make('pages')->title('Pages')->save(); + Facades\Collection::make('articles')->title('Articles')->save(); + + $this + ->prepareNavCaches() + ->get('http://localhost/cp/dashboard') + ->assertStatus(200); + + $collections = $this->buildAndGetItem('Content', 'Collections'); + + $this->assertFalse($collections->isActive()); + $this->assertInstanceOf(Closure::class, $collections->children()); + } + + /** @test */ + public function it_resolves_core_children_closure_and_can_check_when_parent_item_is_active() + { + Facades\Collection::make('pages')->title('Pages')->save(); + Facades\Collection::make('articles')->title('Articles')->save(); + + $this + ->prepareNavCaches() + ->get('http://localhost/cp/collections') + ->assertStatus(200); + + $collections = $this->buildAndGetItem('Content', 'Collections'); + + $this->assertTrue($collections->isActive()); + $this->assertInstanceOf(Collection::class, $collections->children()); + $this->assertFalse($this->getItemByDisplay($collections->children(), 'Pages')->isActive()); + $this->assertFalse($this->getItemByDisplay($collections->children(), 'Articles')->isActive()); + } + + /** @test */ + public function it_resolves_core_children_closure_and_can_check_when_parent_and_child_item_are_active() + { + Facades\Collection::make('pages')->title('Pages')->save(); + Facades\Collection::make('articles')->title('Articles')->save(); + + $this + ->prepareNavCaches() + ->get('http://localhost/cp/collections/articles') + ->assertStatus(200); + + $collections = $this->buildAndGetItem('Content', 'Collections'); + + $this->assertTrue($collections->isActive()); + $this->assertInstanceOf(Collection::class, $collections->children()); + $this->assertFalse($this->getItemByDisplay($collections->children(), 'Pages')->isActive()); + $this->assertTrue($this->getItemByDisplay($collections->children(), 'Articles')->isActive()); + } + + /** @test */ + public function it_resolves_core_children_closure_and_can_check_when_parent_and_descendant_of_child_item_is_active() + { + Facades\Collection::make('pages')->title('Pages')->save(); + Facades\Collection::make('articles')->title('Articles')->save(); + + $this + ->prepareNavCaches() + ->get('http://localhost/cp/collections/articles/entries/create/en') + ->assertStatus(200); + + $collections = $this->buildAndGetItem('Content', 'Collections'); + + $this->assertTrue($collections->isActive()); + $this->assertInstanceOf(Collection::class, $collections->children()); + $this->assertFalse($collections->children()->keyBy->display()->get('Pages')->isActive()); + $this->assertTrue($collections->children()->keyBy->display()->get('Articles')->isActive()); + } + + /** @test */ + public function it_can_check_if_parent_extension_with_array_based_children_item_is_active() + { + Facades\CP\Nav::extend(function ($nav) { + $nav->tools('SEO Pro') + ->url('/cp/seo-pro') + ->children([ + $nav->item('Reports')->url('/cp/seo-pro/reports')->can('view seo reports'), + $nav->item('Section Defaults')->url('/cp/seo-pro/section-defaults')->can('edit seo section defaults'), + ]); + }); + + $this + ->prepareNavCaches() + ->get('http://localhost/cp/seo-pro') + ->assertStatus(200); + + $seoPro = $this->buildAndGetItem('Tools', 'SEO Pro'); + + $this->assertTrue($seoPro->isActive()); + $this->assertInstanceOf(Collection::class, $seoPro->children()); + $this->assertFalse($this->getItemByDisplay($seoPro->children(), 'Reports')->isActive()); + $this->assertFalse($this->getItemByDisplay($seoPro->children(), 'Section Defaults')->isActive()); + } + + /** @test */ + public function it_can_check_when_parent_and_array_based_child_extension_items_are_active() + { + Facades\CP\Nav::extend(function ($nav) { + $nav->tools('SEO Pro') + ->url('/cp/seo-pro') + ->children([ + $nav->item('Reports')->url('/cp/seo-pro/reports')->can('view seo reports'), + $nav->item('Section Defaults')->url('/cp/seo-pro/section-defaults')->can('edit seo section defaults'), + ]); + }); + + $this + ->prepareNavCaches() + ->get('http://localhost/cp/seo-pro/section-defaults') + ->assertStatus(200); + + $seoPro = $this->buildAndGetItem('Tools', 'SEO Pro'); + + $this->assertTrue($seoPro->isActive()); + $this->assertInstanceOf(Collection::class, $seoPro->children()); + $this->assertFalse($this->getItemByDisplay($seoPro->children(), 'Reports')->isActive()); + $this->assertTrue($this->getItemByDisplay($seoPro->children(), 'Section Defaults')->isActive()); + } + + /** @test */ + public function it_can_check_when_parent_and_array_based_descendant_of_child_extension_item_is_active() + { + Facades\CP\Nav::extend(function ($nav) { + $nav->tools('SEO Pro') + ->url('/cp/seo-pro') + ->children([ + $nav->item('Reports')->url('/cp/seo-pro/reports')->can('view seo reports'), + $nav->item('Section Defaults')->url('/cp/seo-pro/section-defaults')->can('edit seo section defaults'), + ]); + }); + + $this + ->prepareNavCaches() + ->get('http://localhost/cp/seo-pro/section-defaults/pages') + ->assertStatus(200); + + $seoPro = $this->buildAndGetItem('Tools', 'SEO Pro'); + + $this->assertTrue($seoPro->isActive()); + $this->assertInstanceOf(Collection::class, $seoPro->children()); + $this->assertFalse($this->getItemByDisplay($seoPro->children(), 'Reports')->isActive()); + $this->assertTrue($this->getItemByDisplay($seoPro->children(), 'Section Defaults')->isActive()); + } + + /** @test */ + public function it_builds_extension_children_closure_when_not_active() + { + Facades\CP\Nav::extend(function ($nav) { + $nav->tools('SEO Pro') + ->url('/cp/seo-pro') + ->children(function () use ($nav) { + return [ + $nav->item('Reports')->url('/cp/seo-pro/')->can('view seo reports'), + $nav->item('Site Defaults')->url('/cp/seo-pro/site-defaults')->can('edit seo site defaults'), + $nav->item('Section Defaults')->url('/cp/seo-pro/section-defaults')->can('edit seo section defaults'), + ]; + }); + }); + + $this + ->prepareNavCaches() + ->get('http://localhost/cp/dashboard') + ->assertStatus(200); + + $seoPro = $this->buildAndGetItem('Tools', 'SEO Pro'); + + $this->assertFalse($seoPro->isActive()); + $this->assertInstanceOf(Closure::class, $seoPro->children()); + } + + /** @test */ + public function it_resolves_extension_children_closure_and_can_check_when_parent_item_is_active() + { + Facades\CP\Nav::extend(function ($nav) { + $nav->tools('SEO Pro') + ->url('/cp/seo-pro') + ->children(function () use ($nav) { + return [ + $nav->item('Reports')->url('/cp/seo-pro/reports')->can('view seo reports'), + $nav->item('Section Defaults')->url('/cp/seo-pro/section-defaults')->can('edit seo section defaults'), + ]; + }); + }); + + $this + ->prepareNavCaches() + ->get('http://localhost/cp/seo-pro') + ->assertStatus(200); + + $seoPro = $this->buildAndGetItem('Tools', 'SEO Pro'); + + $this->assertTrue($seoPro->isActive()); + $this->assertInstanceOf(Collection::class, $seoPro->children()); + $this->assertFalse($this->getItemByDisplay($seoPro->children(), 'Reports')->isActive()); + $this->assertFalse($this->getItemByDisplay($seoPro->children(), 'Section Defaults')->isActive()); + } + + /** @test */ + public function it_resolves_extension_children_closure_and_can_check_when_parent_and_child_item_are_active() + { + Facades\CP\Nav::extend(function ($nav) { + $nav->tools('SEO Pro') + ->url('/cp/seo-pro') + ->children(function () use ($nav) { + return [ + $nav->item('Reports')->url('/cp/seo-pro/reports')->can('view seo reports'), + $nav->item('Section Defaults')->url('/cp/seo-pro/section-defaults')->can('edit seo section defaults'), + ]; + }); + }); + + $this + ->prepareNavCaches() + ->get('http://localhost/cp/seo-pro/section-defaults') + ->assertStatus(200); + + $seoPro = $this->buildAndGetItem('Tools', 'SEO Pro'); + + $this->assertTrue($seoPro->isActive()); + $this->assertInstanceOf(Collection::class, $seoPro->children()); + $this->assertFalse($this->getItemByDisplay($seoPro->children(), 'Reports')->isActive()); + $this->assertTrue($this->getItemByDisplay($seoPro->children(), 'Section Defaults')->isActive()); + } + + /** @test */ + public function it_resolves_extension_children_closure_and_can_check_when_parent_and_descendant_of_child_item_is_active() + { + Facades\CP\Nav::extend(function ($nav) { + $nav->tools('SEO Pro') + ->url('/cp/seo-pro') + ->children(function () use ($nav) { + return [ + $nav->item('Reports')->url('/cp/seo-pro/reports')->can('view seo reports'), + $nav->item('Section Defaults')->url('/cp/seo-pro/section-defaults')->can('edit seo section defaults'), + ]; + }); + }); + + $this + ->prepareNavCaches() + ->get('http://localhost/cp/seo-pro/section-defaults/pages') + ->assertStatus(200); + + $seoPro = $this->buildAndGetItem('Tools', 'SEO Pro'); + + $this->assertTrue($seoPro->isActive()); + $this->assertInstanceOf(Collection::class, $seoPro->children()); + $this->assertFalse($this->getItemByDisplay($seoPro->children(), 'Reports')->isActive()); + $this->assertTrue($this->getItemByDisplay($seoPro->children(), 'Section Defaults')->isActive()); + } + + /** @test */ + public function it_properly_handles_various_edge_cases_when_checking_is_active_on_descendants_of_nav_children() + { + // Ensure urls are not cached so that we can test regex based isActive() checks + Nav::clearCachedUrls(); + + // These patterns are only intended to check against descendants of child items, since we have explicit child URLs + $parent = Nav::create('parent') + ->section('test') + ->url('http://localhost/cp/parent') + ->children([ + $hello = Nav::create('hello')->url('http://localhost/cp/hello'), + $helloWithQueryParams = Nav::create('helloWithAnchor')->url('http://localhost/cp/hello?params'), + $helloWithAnchor = Nav::create('helloWithAnchor')->url('http://localhost/cp/hello#anchor'), + $hell = Nav::create('hell')->url('http://localhost/cp/hell'), + $localNotCp = Nav::create('localNotCp')->url('/dashboard'), + $external = Nav::create('external')->url('http://external.com'), + $externalSecure = Nav::create('externalSecure')->url('https://external.com'), + ]); + + // Test active status on an explicit item + Request::swap(Request::create('http://localhost/cp/hell')); + $this->assertTrue($parent->isActive()); + $this->assertFalse($hello->isActive()); + $this->assertFalse($helloWithQueryParams->isActive()); + $this->assertFalse($helloWithAnchor->isActive()); + $this->assertTrue($hell->isActive()); + $this->assertFalse($localNotCp->isActive()); + $this->assertFalse($external->isActive()); + $this->assertFalse($externalSecure->isActive()); + + // Test active status on an explicit item where url params or anchors were set on url + Request::swap(Request::create('http://localhost/cp/hello')); + $this->assertTrue($parent->isActive()); + $this->assertTrue($hello->isActive()); + $this->assertTrue($helloWithQueryParams->isActive()); + $this->assertTrue($helloWithAnchor->isActive()); + $this->assertFalse($hell->isActive()); + $this->assertFalse($localNotCp->isActive()); + $this->assertFalse($external->isActive()); + $this->assertFalse($externalSecure->isActive()); + + // Test active status on a descendant of an explicit item + Request::swap(Request::create('http://localhost/cp/hell/test')); + $this->assertTrue($parent->isActive()); + $this->assertFalse($hello->isActive()); + $this->assertFalse($helloWithQueryParams->isActive()); + $this->assertFalse($helloWithAnchor->isActive()); + $this->assertTrue($hell->isActive()); + $this->assertFalse($localNotCp->isActive()); + $this->assertFalse($external->isActive()); + $this->assertFalse($externalSecure->isActive()); + + // Test active status on a descendant of an explicit item where url params or anchors were set on url + Request::swap(Request::create('http://localhost/cp/hello/test')); + $this->assertTrue($parent->isActive()); + $this->assertTrue($hello->isActive()); + $this->assertTrue($helloWithQueryParams->isActive()); + $this->assertTrue($helloWithAnchor->isActive()); + $this->assertFalse($hell->isActive()); + $this->assertFalse($localNotCp->isActive()); + $this->assertFalse($external->isActive()); + $this->assertFalse($externalSecure->isActive()); + + // Test active status on a descendant of an explicit item where url param is part of current url + Request::swap(Request::create('http://localhost/cp/hello?params')); + $this->assertTrue($parent->isActive()); + $this->assertTrue($hello->isActive()); + $this->assertTrue($helloWithQueryParams->isActive()); + $this->assertTrue($helloWithAnchor->isActive()); + $this->assertFalse($hell->isActive()); + $this->assertFalse($localNotCp->isActive()); + $this->assertFalse($external->isActive()); + $this->assertFalse($externalSecure->isActive()); + + // Test active status on a descendant of an explicit item where anchor is part of current url + Request::swap(Request::create('http://localhost/cp/hello#anchor')); + $this->assertTrue($parent->isActive()); + $this->assertTrue($hello->isActive()); + $this->assertTrue($helloWithQueryParams->isActive()); + $this->assertTrue($helloWithAnchor->isActive()); + $this->assertFalse($hell->isActive()); + $this->assertFalse($localNotCp->isActive()); + $this->assertFalse($external->isActive()); + $this->assertFalse($externalSecure->isActive()); + + // Test active status on a deeper descendant of an explicit item where url params and anchors were set on url + Request::swap(Request::create('http://localhost/cp/hello/this/is/super/nested?params#anchor')); + $this->assertTrue($parent->isActive()); + $this->assertTrue($hello->isActive()); + $this->assertTrue($helloWithQueryParams->isActive()); + $this->assertTrue($helloWithAnchor->isActive()); + $this->assertFalse($hell->isActive()); + $this->assertFalse($localNotCp->isActive()); + $this->assertFalse($external->isActive()); + $this->assertFalse($externalSecure->isActive()); + + // Ensure regex check is not used when checking is active on explicit parent item + Request::swap(Request::create('http://localhost/cp/parent')); + $this->assertTrue($parent->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()); + $this->assertFalse($externalSecure->isActive()); + + // Ensure regex check is not used when checking is active on descendant of parent item + Request::swap(Request::create('http://localhost/cp/parent/nested/item')); + $this->assertFalse($parent->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()); + $this->assertFalse($externalSecure->isActive()); + } + + /** @test */ + public function active_nav_check_still_functions_properly_when_custom_nav_extension_hijacks_a_core_item_child() + { + Facades\Collection::make('pages')->title('Pages')->save(); + Facades\Collection::make('articles')->title('Articles')->save(); + Facades\Collection::make('products')->title('Products')->save(); + + Facades\Taxonomy::make('tags')->title('Tags')->save(); + Facades\Taxonomy::make('categories')->title('Categories')->save(); + + // Remove `Products` and `Categories` from core parents, and add to `Schopify` extension item as children + Facades\CP\Nav::extend(function ($nav) { + $nav->remove('Content', 'Collections', 'Products'); + $nav->remove('Content', 'Taxonomies', 'Categories'); + + $nav->tools('Schopify') + ->url('/cp/collections/products') + ->children(function () use ($nav) { + return [ + $nav->item('Products')->url('/cp/collections/products'), + $nav->item('Categories')->url('/cp/taxonomies/categories'), + ]; + }); + }); + + $this + ->prepareNavCaches() + ->get('http://localhost/cp/collections/products') + ->assertStatus(200); + + $nav = $this->build(); + + $collections = $this->getItemByDisplay($nav->get('Content'), 'Collections'); + $taxonomies = $this->getItemByDisplay($nav->get('Content'), 'Taxonomies'); + $schopify = $this->getItemByDisplay($nav->get('Tools'), 'Schopify'); + + // Ensure only the `Schopify` nav item is active, since we moved the current url (ie. `Products`) to this item + $this->assertFalse($collections->isActive()); + $this->assertFalse($taxonomies->isActive()); + $this->assertTrue($schopify->isActive()); + $this->assertInstanceOf(Collection::class, $schopify->children()); + + // Ensure the new `Products` child under `Schopify` is active + $this->assertTrue($this->getItemByDisplay($schopify->children(), 'Products')->isActive()); + $this->assertFalse($this->getItemByDisplay($schopify->children(), 'Categories')->isActive()); + + // Ensure hijacked items were properly removed from original parents + $this->assertInstanceOf(Collection::class, $collections->children()); + $this->assertEquals(['Articles', 'Pages'], $collections->children()->map->display()->all()); + $this->assertInstanceOf(Collection::class, $taxonomies->children()); + $this->assertEquals(['Tags'], $taxonomies->children()->map->display()->all()); + } + + /** @test */ + public function active_nav_descendant_check_still_functions_properly_when_custom_nav_extension_hijacks_a_core_item_child() + { + Facades\Collection::make('pages')->title('Pages')->save(); + Facades\Collection::make('articles')->title('Articles')->save(); + Facades\Collection::make('products')->title('Products')->save(); + + Facades\Taxonomy::make('tags')->title('Tags')->save(); + Facades\Taxonomy::make('categories')->title('Categories')->save(); + + // Remove `Products` and `Categories` from core parents, and add to `Schopify` extension item as children + Facades\CP\Nav::extend(function ($nav) { + $nav->remove('Content', 'Collections', 'Products'); + $nav->remove('Content', 'Taxonomies', 'Categories'); + + $nav->tools('Schopify') + ->url('/cp/collections/products') + ->children(function () use ($nav) { + return [ + $nav->item('Products')->url('/cp/collections/products'), + $nav->item('Categories')->url('/cp/taxonomies/categories'), + ]; + }); + }); + + $this + ->prepareNavCaches() + ->get('http://localhost/cp/collections/products/entries/create/en') + ->assertStatus(200); + + $nav = $this->build(); + + $collections = $this->getItemByDisplay($nav->get('Content'), 'Collections'); + $taxonomies = $this->getItemByDisplay($nav->get('Content'), 'Taxonomies'); + $schopify = $this->getItemByDisplay($nav->get('Tools'), 'Schopify'); + + // Ensure only the `Schopify` nav item is active, since we moved the current url (ie. `Products`) to this item + $this->assertFalse($collections->isActive()); + $this->assertFalse($taxonomies->isActive()); + $this->assertTrue($schopify->isActive()); + $this->assertInstanceOf(Collection::class, $schopify->children()); + + // Ensure the new `Products` child under `Schopify` is active, because the current URL is a descendant of this item + $this->assertTrue($this->getItemByDisplay($schopify->children(), 'Products')->isActive()); + $this->assertFalse($this->getItemByDisplay($schopify->children(), 'Categories')->isActive()); + } + + /** @test */ + public function active_nav_descendant_with_unrelated_url_still_functions_properly_when_custom_nav_extension_hijacks_a_core_item_child() + { + Facades\Collection::make('pages')->title('Pages')->save(); + Facades\Collection::make('articles')->title('Articles')->save(); + Facades\Collection::make('products')->title('Products')->save(); + + Facades\Taxonomy::make('tags')->title('Tags')->save(); + Facades\Taxonomy::make('categories')->title('Categories')->save(); + + // Remove `Products` and `Categories` from core parents, and add to `Schopify` extension item as children + Facades\CP\Nav::extend(function ($nav) { + $nav->remove('Content', 'Collections', 'Products'); + $nav->remove('Content', 'Taxonomies', 'Categories'); + + $nav->tools('Schopify') + ->url('/cp/collections/products') + ->children(function () use ($nav) { + return [ + $nav->item('Products')->url('/cp/collections/products'), + $nav->item('Categories')->url('/cp/taxonomies/categories'), + $nav->item('Unrelated')->url('/cp/totally-custom-url'), + ]; + }); + }); + + $this + ->prepareNavCaches() + ->get('http://localhost/cp/totally-custom-url/deeper/descendant') + ->assertStatus(200); + + $schopify = $this->buildAndGetItem('Tools', 'Schopify'); + + // Ensure only the `Schopify` nav item is active and children are resolved + $this->assertTrue($schopify->isActive()); + $this->assertInstanceOf(Collection::class, $schopify->children()); + + // Ensure our `Unrelated` totally custom URL item is considered active as well, based on URL hierarchy + $this->assertFalse($this->getItemByDisplay($schopify->children(), 'Products')->isActive()); + $this->assertFalse($this->getItemByDisplay($schopify->children(), 'Categories')->isActive()); + $this->assertTrue($this->getItemByDisplay($schopify->children(), 'Unrelated')->isActive()); + } + + /** @test */ + public function active_nav_check_still_functions_properly_on_moved_items() + { + Facades\Collection::make('pages')->title('Pages')->save(); + Facades\Collection::make('articles')->title('Articles')->save(); + + Facades\Taxonomy::make('tags')->title('Tags')->save(); + Facades\Taxonomy::make('categories')->title('Categories')->save(); + + $this + ->prepareNavCaches() + ->get('http://localhost/cp/collections/articles') + ->assertStatus(200); + + $nav = $this->build([ + 'top_level' => [ + 'content::collections::articles' => [ + 'action' => '@move', + 'children' => [ + 'content::taxonomies::categories' => '@move', + ], + ], + ], + ]); + + $articles = $this->getItemByDisplay($nav->get('Top Level'), 'Articles'); + $categories = $this->getItemByDisplay($articles->children(), 'Categories'); + $collections = $this->getItemByDisplay($nav->get('Content'), 'Collections'); + $taxonomies = $this->getItemByDisplay($nav->get('Content'), 'Taxonomies'); + + // Ensure old parents are not active + $this->assertFalse($collections->isActive()); + $this->assertFalse($taxonomies->isActive()); + + // Ensure moved item is active + $this->assertTrue($articles->isActive()); + + // Child should not be active in this case though + $this->assertFalse($categories->isActive()); + } + + /** @test */ + public function active_nav_check_still_functions_properly_on_explicit_child_within_moved_items() + { + Facades\Collection::make('pages')->title('Pages')->save(); + Facades\Collection::make('articles')->title('Articles')->save(); + + Facades\Taxonomy::make('tags')->title('Tags')->save(); + Facades\Taxonomy::make('categories')->title('Categories')->save(); + + $this + ->prepareNavCaches() + ->get('http://localhost/cp/taxonomies/categories') + ->assertStatus(200); + + $nav = $this->build([ + 'top_level' => [ + 'content::collections::articles' => [ + 'action' => '@move', + 'children' => [ + 'content::taxonomies::categories' => '@move', + ], + ], + ], + ]); + + $articles = $this->getItemByDisplay($nav->get('Top Level'), 'Articles'); + $categories = $this->getItemByDisplay($articles->children(), 'Categories'); + $collections = $this->getItemByDisplay($nav->get('Content'), 'Collections'); + $taxonomies = $this->getItemByDisplay($nav->get('Content'), 'Taxonomies'); + + // Ensure old parents are not active + $this->assertFalse($collections->isActive()); + $this->assertFalse($taxonomies->isActive()); + + // Ensure moved item is active + $this->assertTrue($articles->isActive()); + + // Ensure child of moved item is now active + $this->assertTrue($categories->isActive()); + } + + /** @test */ + public function active_nav_check_still_functions_properly_on_descendant_of_moved_items() + { + Facades\Collection::make('pages')->title('Pages')->save(); + Facades\Collection::make('articles')->title('Articles')->save(); + + Facades\Taxonomy::make('tags')->title('Tags')->save(); + Facades\Taxonomy::make('categories')->title('Categories')->save(); + + $this + ->prepareNavCaches() + ->get('http://localhost/cp/collections/articles/entries/create/en') + ->assertStatus(200); + + $nav = $this->build([ + 'top_level' => [ + 'content::collections::articles' => [ + 'action' => '@move', + 'children' => [ + 'content::taxonomies::categories' => '@move', + ], + ], + ], + ]); + + $articles = $this->getItemByDisplay($nav->get('Top Level'), 'Articles'); + $categories = $this->getItemByDisplay($articles->children(), 'Categories'); + $collections = $this->getItemByDisplay($nav->get('Content'), 'Collections'); + $taxonomies = $this->getItemByDisplay($nav->get('Content'), 'Taxonomies'); + + // Ensure old parents are not active + $this->assertFalse($collections->isActive()); + $this->assertFalse($taxonomies->isActive()); + + // Ensure moved item is active, due to URL hierarchy of current URL being a descendant + $this->assertTrue($articles->isActive()); + + // Child should not be active in this case though + $this->assertFalse($categories->isActive()); + } + + /** @test */ + public function active_nav_check_still_functions_properly_on_descendant_of_child_within_moved_item() + { + Facades\Collection::make('pages')->title('Pages')->save(); + Facades\Collection::make('articles')->title('Articles')->save(); + + Facades\Taxonomy::make('tags')->title('Tags')->save(); + Facades\Taxonomy::make('categories')->title('Categories')->save(); + + $this + ->prepareNavCaches() + ->get('http://localhost/cp/taxonomies/categories/terms/create/en') + ->assertStatus(200); + + $nav = $this->build([ + 'top_level' => [ + 'content::collections::articles' => [ + 'action' => '@move', + 'children' => [ + 'content::taxonomies::categories' => '@move', + ], + ], + ], + ]); + + $articles = $this->getItemByDisplay($nav->get('Top Level'), 'Articles'); + $categories = $this->getItemByDisplay($articles->children(), 'Categories'); + $collections = $this->getItemByDisplay($nav->get('Content'), 'Collections'); + $taxonomies = $this->getItemByDisplay($nav->get('Content'), 'Taxonomies'); + + // Ensure old parents are not active + $this->assertFalse($collections->isActive()); + $this->assertFalse($taxonomies->isActive()); + + // Ensure moved item is active + $this->assertTrue($articles->isActive()); + + // Child should not be active in this case, due to URL hierarchy of current URL being a descendant + $this->assertTrue($categories->isActive()); + } + + protected function prepareNavCaches() + { + // Clear caches + Nav::clearCachedUrls(); + $this->assertFalse(Cache::has(NavBuilder::UNRESOLVED_CHILDREN_URLS_CACHE_KEY)); + $this->assertFalse(Blink::has(NavBuilder::UNRESOLVED_CHILDREN_URLS_CACHE_KEY)); + $this->assertFalse(Cache::has(NavBuilder::ALL_URLS_CACHE_KEY)); + $this->assertFalse(Blink::has(NavBuilder::ALL_URLS_CACHE_KEY)); + + // Ensure the nav is built and cached so that tests can check `isActive()` on children in unresolved closures + Nav::build(); + $this->assertTrue(Cache::has(NavBuilder::UNRESOLVED_CHILDREN_URLS_CACHE_KEY)); + $this->assertTrue(Blink::has(NavBuilder::UNRESOLVED_CHILDREN_URLS_CACHE_KEY)); + $this->assertTrue(Cache::has(NavBuilder::ALL_URLS_CACHE_KEY)); + $this->assertTrue(Blink::has(NavBuilder::ALL_URLS_CACHE_KEY)); + + return $this; + } + + protected function build($preferences = null) + { + return Nav::build($preferences)->pluck('items', 'display'); + } + + protected function buildAndGetItem($sectionDisplay, $itemDisplay) + { + $sectionItems = $this->build()->get($sectionDisplay); + + return $this->getItemByDisplay($sectionItems, $itemDisplay); + } + + protected function getItemByDisplay($items, $display) + { + return $items->keyBy->display()->get($display); + } +} diff --git a/tests/CP/Navigation/NavPreferencesTest.php b/tests/CP/Navigation/NavPreferencesTest.php index 5b3b64510e1..e5389da5820 100644 --- a/tests/CP/Navigation/NavPreferencesTest.php +++ b/tests/CP/Navigation/NavPreferencesTest.php @@ -503,8 +503,8 @@ public function it_can_alias_items_within_a_section() ], ]); $this->assertEquals(['Collections', 'Navigation', 'Taxonomies', 'Assets', 'Globals', 'Pages'], $nav->get('Content')->map->display()->all()); - $this->assertArrayHasKey('Pages', $nav->get('Content')->keyBy->display()->get('Collections')->children()->keyBy->display()->all()); - $this->assertArrayHasKey('Articles', $nav->get('Content')->keyBy->display()->get('Collections')->children()->keyBy->display()->all()); + $this->assertArrayHasKey('Pages', $nav->get('Content')->keyBy->display()->get('Collections')->resolveChildren()->children()->keyBy->display()->all()); + $this->assertArrayHasKey('Articles', $nav->get('Content')->keyBy->display()->get('Collections')->resolveChildren()->children()->keyBy->display()->all()); } /** @test */ @@ -596,8 +596,8 @@ public function it_can_alias_items_into_another_section() ], ]); $this->assertEquals(['Dashboard', 'Pages'], $nav->get('Top Level')->map->display()->all()); - $this->assertArrayHasKey('Pages', $nav->get('Content')->keyBy->display()->get('Collections')->children()->keyBy->display()->all()); - $this->assertArrayHasKey('Articles', $nav->get('Content')->keyBy->display()->get('Collections')->children()->keyBy->display()->all()); + $this->assertArrayHasKey('Pages', $nav->get('Content')->keyBy->display()->get('Collections')->resolveChildren()->children()->keyBy->display()->all()); + $this->assertArrayHasKey('Articles', $nav->get('Content')->keyBy->display()->get('Collections')->resolveChildren()->children()->keyBy->display()->all()); // Aliasing in same section should just copy the item... $nav = $this->buildNavWithPreferences([ diff --git a/tests/CP/Navigation/NavTest.php b/tests/CP/Navigation/NavTest.php index 3b9c9f885b6..be2e5656aaa 100644 --- a/tests/CP/Navigation/NavTest.php +++ b/tests/CP/Navigation/NavTest.php @@ -2,7 +2,6 @@ namespace Tests\CP\Navigation; -use Illuminate\Support\Facades\Request; use Illuminate\Support\Facades\Route; use Statamic\CP\Navigation\NavItem; use Statamic\Facades; @@ -461,132 +460,6 @@ public function it_can_use_extend_to_remove_a_default_statamic_child_nav_item() $this->assertEquals(['Pages'], $collectionsChildren()->map->display()->all()); } - /** @test */ - public function it_checks_if_active() - { - $hello = Nav::create('hello')->url('http://localhost/cp/hello'); - $helloWithQueryParams = Nav::create('helloWithAnchor')->url('http://localhost/cp/hello?params'); - $helloWithAnchor = Nav::create('helloWithAnchor')->url('http://localhost/cp/hello#anchor'); - $hell = Nav::create('hell')->url('http://localhost/cp/hell'); - $localNotCp = Nav::create('localNotCp')->url('/dashboard'); - $external = Nav::create('external')->url('http://external.com'); - $externalSecure = Nav::create('externalSecure')->url('https://external.com'); - - Request::swap(Request::create('http://localhost/cp/hell')); - $this->assertFalse($hello->isActive()); - $this->assertFalse($helloWithQueryParams->isActive()); - $this->assertFalse($helloWithAnchor->isActive()); - $this->assertTrue($hell->isActive()); - $this->assertFalse($localNotCp->isActive()); - $this->assertFalse($external->isActive()); - $this->assertFalse($externalSecure->isActive()); - - Request::swap(Request::create('http://localhost/cp/hello')); - $this->assertTrue($hello->isActive()); - $this->assertTrue($helloWithQueryParams->isActive()); - $this->assertTrue($helloWithAnchor->isActive()); - $this->assertFalse($hell->isActive()); - $this->assertFalse($localNotCp->isActive()); - $this->assertFalse($external->isActive()); - $this->assertFalse($externalSecure->isActive()); - - Request::swap(Request::create('http://localhost/cp/hell/test')); - $this->assertFalse($hello->isActive()); - $this->assertFalse($helloWithQueryParams->isActive()); - $this->assertFalse($helloWithAnchor->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->assertFalse($hello->isActive()); - $this->assertFalse($helloWithQueryParams->isActive()); - $this->assertFalse($helloWithAnchor->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?params')); - $this->assertTrue($hello->isActive()); - $this->assertTrue($helloWithQueryParams->isActive()); - $this->assertTrue($helloWithAnchor->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#anchor')); - $this->assertTrue($hello->isActive()); - $this->assertTrue($helloWithQueryParams->isActive()); - $this->assertTrue($helloWithAnchor->isActive()); - $this->assertFalse($hell->isActive()); - $this->assertFalse($localNotCp->isActive()); - $this->assertFalse($external->isActive()); - $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() {