Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[4.x] Fix CP nav item active status regressions #8880

Merged
merged 29 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b8d0361
Whoops, this isn’t needed anymore.
jesseleite Sep 21, 2023
09d5b48
Ensure we cache explicit cp nav urls for `isActive()` checks on unres…
jesseleite Oct 21, 2023
85462c3
Refactor `isActive()` logic.
jesseleite Oct 21, 2023
483aaa0
This isn’t being used anymore.
jesseleite Oct 21, 2023
e767756
Ensure we bust caches when updating cp nav preferences.
jesseleite Oct 21, 2023
34d8fef
Tests wip.
jesseleite Oct 21, 2023
2c66d0c
Cache before applying preference overrides, so that cache is applicab…
jesseleite Oct 21, 2023
2e8db19
Let `NavBuilder` handle caching.
jesseleite Oct 24, 2023
620f03b
Fix edge case `isActive` bug when moving an item out of a core nav ch…
jesseleite Oct 24, 2023
e9ec6e0
Merge branch '4.x' of https://github.com/statamic/cms into fix/cp-nav…
jesseleite Oct 24, 2023
974e843
Be smarter about updating closure based children URL caches when item…
jesseleite Oct 25, 2023
4489397
Deprecate this, because it’s confusing all around.
jesseleite Oct 25, 2023
dd00e79
Add arg to easily remove child CP nav item.
jesseleite Oct 25, 2023
8ef2e15
Test new public `find()` method as well.
jesseleite Oct 25, 2023
7774a56
Fix more edge cases.
jesseleite Oct 27, 2023
423af5d
Fix more edge cases around URL hierarchy.
jesseleite Oct 27, 2023
7cdc675
These patterns are only intended to check against descendants of chil…
jesseleite Oct 27, 2023
38cf8f9
Active nav item tests wip.
jesseleite Oct 31, 2023
0e1ee72
Merge branch '4.x' of https://github.com/statamic/cms into fix/cp-nav…
jesseleite Oct 31, 2023
4d4b2df
Merge branch 'features/add-arg-to-easily-remove-child-cp-nav-item' in…
jesseleite Oct 31, 2023
73a5782
Rename
jesseleite Oct 31, 2023
564451e
Add test coverage for extension nav items and children.
jesseleite Oct 31, 2023
be9c6d1
Test rad-pack/shopify case where we’re hijacking core item children.
jesseleite Oct 31, 2023
fd4acaf
Rename.
jesseleite Oct 31, 2023
4f8cc9d
Test that descendant `isActive()` check works on URLs unrelated to pa…
jesseleite Oct 31, 2023
de54d6e
Ensure hijacked items were properly removed from original parents
jesseleite Oct 31, 2023
948e7f5
Add better test coverage for the use case mentioned by the original f…
jesseleite Nov 1, 2023
0ee1e0e
Test that caches get updated when new children are resolved.
jesseleite Nov 1, 2023
5274872
Merge branch '4.x' of https://github.com/statamic/cms into fix/cp-nav…
jesseleite Nov 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/CP/Navigation/Nav.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
170 changes: 167 additions & 3 deletions src/CP/Navigation/NavBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,29 @@
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;
use Statamic\Support\Str;

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;

/**
Expand Down Expand Up @@ -45,25 +53,42 @@ public function build($preferences = true)
}

return $this
->buildChildren()
->trackChildrenClosures()
->resolveChildrenClosures()
->validateNesting()
->validateViews()
->authorizeItems()
->authorizeChildren()
->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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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.
*
Expand Down
Loading
Loading