From 9c9cb22f7bd9b2d488eacaad7177967f1da59b9d Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Sat, 7 Nov 2026 02:12:19 +0000 Subject: [PATCH] Look up each tree page's entry once when building the CP tree --- src/Structures/TreeBuilder.php | 30 ++-- .../Navigation/ViewNavigationTreeTest.php | 131 ++++++++++++++++++ 2 files changed, 150 insertions(+), 11 deletions(-) create mode 100644 tests/Feature/Navigation/ViewNavigationTreeTest.php diff --git a/src/Structures/TreeBuilder.php b/src/Structures/TreeBuilder.php index c6ebac6b2a8..47856fc2c77 100644 --- a/src/Structures/TreeBuilder.php +++ b/src/Structures/TreeBuilder.php @@ -87,26 +87,32 @@ public function buildForController($params) protected function transformTreeForController($tree) { - return collect($tree)->map(function ($item) { + $user = User::current(); + $transformed = []; + + foreach ($tree as $item) { $page = $item['page']; $collection = $page->mountedCollection(); - $referenceExists = $page->referenceExists(); - return [ + // Look the entry and its blueprint up once per page, rather than for every key. + $entry = $page->entry(); + $blueprint = $entry?->blueprint(); + + $transformed[] = [ 'id' => $page->id(), 'entry' => $page->reference(), 'title' => $page->hasCustomTitle() ? $page->title() : null, - 'entry_title' => $referenceExists ? $page->entry()->value('title') : null, - 'entry_blueprint' => $referenceExists ? [ - 'handle' => $page->entry()->blueprint()->handle(), - 'title' => $page->entry()->blueprint()->title(), + 'entry_title' => $entry ? $entry->value('title') : null, + 'entry_blueprint' => $entry ? [ + 'handle' => $blueprint->handle(), + 'title' => $blueprint->title(), ] : null, 'url' => $page->url(), 'edit_url' => $page->editUrl(), - 'can_delete' => $referenceExists ? User::current()->can('delete', $page->entry()) : true, + 'can_delete' => $entry ? $user->can('delete', $entry) : true, 'slug' => $page->slug(), - 'status' => $referenceExists ? $page->status() : null, - 'redirect' => $referenceExists ? $page->entry()->get('redirect') : null, + 'status' => $entry ? $page->status() : null, + 'redirect' => $entry ? $entry->get('redirect') : null, 'collection' => ! $collection ? null : [ 'handle' => $collection->handle(), 'title' => $collection->title(), @@ -115,6 +121,8 @@ protected function transformTreeForController($tree) ], 'children' => (! empty($item['children'])) ? $this->transformTreeForController($item['children']) : [], ]; - })->values()->all(); + } + + return $transformed; } } diff --git a/tests/Feature/Navigation/ViewNavigationTreeTest.php b/tests/Feature/Navigation/ViewNavigationTreeTest.php new file mode 100644 index 00000000000..ca98dc9632e --- /dev/null +++ b/tests/Feature/Navigation/ViewNavigationTreeTest.php @@ -0,0 +1,131 @@ +createNavigation(); + + $this + ->actingAs(tap(User::make()->makeSuper())->save()) + ->getJson(cp_route('navigation.tree.index', ['navigation' => 'main', 'site' => 'en'])) + ->assertOk() + ->assertExactJson(['pages' => $this->expectedPages(canDelete: true)]); + } + + #[Test] + public function it_checks_whether_the_user_can_delete_each_entry() + { + $this->createNavigation(); + + $this->setTestRoles(['test' => ['access cp', 'view main nav', 'edit main nav', 'view pages entries', 'edit pages entries']]); + + $this + ->actingAs(tap(User::make()->assignRole('test'))->save()) + ->getJson(cp_route('navigation.tree.index', ['navigation' => 'main', 'site' => 'en'])) + ->assertOk() + ->assertExactJson(['pages' => $this->expectedPages(canDelete: false)]); + } + + private function createNavigation() + { + $collection = tap(Collection::make('pages')->routes('/{slug}'))->save(); + EntryFactory::id('about')->collection($collection)->slug('about')->data(['title' => 'About'])->create(); + EntryFactory::id('team')->collection($collection)->slug('team')->data(['title' => 'Team', 'redirect' => '/elsewhere'])->published(false)->create(); + + $nav = tap(Nav::make('main')->title('Main')->collections(['pages']))->save(); + + $nav->makeTree('en', [ + ['id' => 'one', 'entry' => 'about', 'children' => [ + ['id' => 'two', 'entry' => 'team', 'title' => 'Our team'], + ]], + ['id' => 'three', 'title' => 'External', 'url' => 'https://example.com'], + ['id' => 'four', 'title' => 'Just text'], + ['id' => 'five', 'entry' => 'missing'], + ])->save(); + } + + private function expectedPages(bool $canDelete) + { + $blueprint = ['handle' => 'page', 'title' => 'Page']; + + return [ + [ + 'id' => 'one', + 'entry' => 'about', + 'title' => null, + 'entry_title' => 'About', + 'entry_blueprint' => $blueprint, + 'url' => '/about', + 'edit_url' => cp_route('collections.entries.edit', ['pages', 'about']), + 'can_delete' => $canDelete, + 'slug' => 'about', + 'status' => 'published', + 'redirect' => null, + 'collection' => null, + 'children' => [ + [ + 'id' => 'two', + 'entry' => 'team', + 'title' => 'Our team', + 'entry_title' => 'Team', + 'entry_blueprint' => $blueprint, + 'url' => '/elsewhere', + 'edit_url' => cp_route('collections.entries.edit', ['pages', 'team']), + 'can_delete' => $canDelete, + 'slug' => 'team', + 'status' => 'draft', + 'redirect' => '/elsewhere', + 'collection' => null, + 'children' => [], + ], + ], + ], + [ + 'id' => 'three', + 'entry' => null, + 'title' => 'External', + 'entry_title' => null, + 'entry_blueprint' => null, + 'url' => 'https://example.com', + 'edit_url' => null, + 'can_delete' => true, + 'slug' => null, + 'status' => null, + 'redirect' => null, + 'collection' => null, + 'children' => [], + ], + [ + 'id' => 'four', + 'entry' => null, + 'title' => 'Just text', + 'entry_title' => null, + 'entry_blueprint' => null, + 'url' => null, + 'edit_url' => null, + 'can_delete' => true, + 'slug' => null, + 'status' => null, + 'redirect' => null, + 'collection' => null, + 'children' => [], + ], + ]; + } +}