From b986eee58f0becf8aa3e3925fb402a74895af5be Mon Sep 17 00:00:00 2001 From: ahmed-rashad-alnaggar <131385452+ahmed-rashad-alnaggar@users.noreply.github.com> Date: Sat, 26 Sep 2026 18:07:21 +0300 Subject: [PATCH] feat: Add strict hierarchical breadcrumbs option (#20153) * Add strict hierarchical option to breadcrumbs method Signed-off-by: ahmed-rashad-alnaggar <131385452+ahmed-rashad-alnaggar@users.noreply.github.com> * Update Page.php Signed-off-by: ahmed-rashad-alnaggar <131385452+ahmed-rashad-alnaggar@users.noreply.github.com> * Update Page.php Signed-off-by: ahmed-rashad-alnaggar <131385452+ahmed-rashad-alnaggar@users.noreply.github.com> * Update Page.php Signed-off-by: ahmed-rashad-alnaggar <131385452+ahmed-rashad-alnaggar@users.noreply.github.com> * Add strict hierarchical breadcrumbs documentation Signed-off-by: ahmed-rashad-alnaggar <131385452+ahmed-rashad-alnaggar@users.noreply.github.com> * Implement getClusterHierarchicalBreadcrumbs method Add method to retrieve hierarchical breadcrumbs for clusters. Signed-off-by: ahmed-rashad-alnaggar <131385452+ahmed-rashad-alnaggar@users.noreply.github.com> * Update Page.php Signed-off-by: ahmed-rashad-alnaggar <131385452+ahmed-rashad-alnaggar@users.noreply.github.com> * Update Page.php Signed-off-by: ahmed-rashad-alnaggar <131385452+ahmed-rashad-alnaggar@users.noreply.github.com> * Add strict hierarchical breadcrumb coverage * Improve navigation hierarchy breadcrumbs * Refine custom page breadcrumb defaults * Support clustered resource record breadcrumbs --------- Signed-off-by: ahmed-rashad-alnaggar <131385452+ahmed-rashad-alnaggar@users.noreply.github.com> Co-authored-by: Dan Harrin --- docs/06-navigation/01-overview.md | 34 ++ packages/panels/src/Pages/Page.php | 170 +++++++++- .../src/Panel/Concerns/HasBreadcrumbs.php | 10 +- packages/panels/src/Resources/Pages/Page.php | 49 ++- .../Panels/Configuration/NavigationTest.php | 20 ++ tests/src/Panels/Pages/BreadcrumbsTest.php | 309 ++++++++++++++++++ .../Panels/Resources/NestedResourceTest.php | 17 + 7 files changed, 602 insertions(+), 7 deletions(-) create mode 100644 tests/src/Panels/Pages/BreadcrumbsTest.php diff --git a/docs/06-navigation/01-overview.md b/docs/06-navigation/01-overview.md index da6b5cc2fb..391b33db33 100644 --- a/docs/06-navigation/01-overview.md +++ b/docs/06-navigation/01-overview.md @@ -639,6 +639,40 @@ public function panel(Panel $panel): Panel } ``` +## Adding a breadcrumb to a custom page + +Custom pages do not include a breadcrumb for the current page by default. You may add one using the `$breadcrumb` property: + +```php +use Filament\Pages\Page; + +class Settings extends Page +{ + protected static ?string $breadcrumb = 'Settings'; + + // ... +} +``` + +## Including the navigation hierarchy in breadcrumbs + +By default, breadcrumbs for a page are built from its resource and parent record relationships. For example, an `EditRecord` page's breadcrumbs are derived from its resource's list and view pages, and any parent resources in the case of nested resources. + +If you would also like breadcrumbs to reflect the full navigation hierarchy of a page, including its [cluster](clusters), [navigation group](#grouping-navigation-items), and [navigation parent item](#grouping-navigation-items-under-other-items), you can enable this behavior in your [configuration](../panel-configuration): + +```php +use Filament\Panel; + +public function panel(Panel $panel): Panel +{ + return $panel + // ... + ->breadcrumbs(hasNavigationHierarchy: true); +} +``` + +When enabled, the navigation hierarchy is prepended to the page's existing breadcrumbs. If a custom page does not have a `$breadcrumb`, its title is used as the current breadcrumb when its navigation hierarchy can be resolved. If the page is not present in the navigation, its existing breadcrumbs are used instead. + ## Reloading the sidebar and topbar Once a page in the panel is loaded, the sidebar and topbar are not reloaded until you navigate away from the page, or until a menu item is clicked to trigger an action. You can manually reload these components to update them by dispatching a `refresh-sidebar` or `refresh-topbar` browser event. diff --git a/packages/panels/src/Pages/Page.php b/packages/panels/src/Pages/Page.php index 9f9e4e1e31..aff90da937 100644 --- a/packages/panels/src/Pages/Page.php +++ b/packages/panels/src/Pages/Page.php @@ -9,6 +9,7 @@ use Filament\Actions\Action; use Filament\Actions\ActionGroup; use Filament\Clusters\Cluster; use Filament\Facades\Filament; +use Filament\Navigation\NavigationGroup; use Filament\Navigation\NavigationItem; use Filament\Panel; use Filament\Schemas\Components\Component; @@ -45,6 +46,8 @@ abstract class Page extends BasePage */ protected static ?string $cluster = null; + protected static ?string $breadcrumb = null; + protected static bool $isDiscovered = true; /** @@ -179,16 +182,175 @@ abstract class Page extends BasePage return $panel->generateRouteName($routeName); } + public function getBreadcrumb(): ?string + { + return static::$breadcrumb; + } + /** - * @return array + * @return array */ public function getBreadcrumbs(): array { - if (filled($cluster = static::getCluster())) { - return $cluster::unshiftClusterBreadcrumbs([]); + $breadcrumb = $this->getBreadcrumb(); + + if (Filament::getCurrentOrDefaultPanel()->hasNavigationHierarchyInBreadcrumbs()) { + $navigationHierarchyBreadcrumbs = $this->getNavigationHierarchyBreadcrumbs(); + + if ($navigationHierarchyBreadcrumbs !== null) { + return [ + ...$navigationHierarchyBreadcrumbs, + $breadcrumb ?? $this->getTitle(), + ]; + } } - return []; + $breadcrumbs = filled($cluster = static::getCluster()) + ? $cluster::unshiftClusterBreadcrumbs([]) + : []; + + if ($breadcrumb !== null) { + $breadcrumbs[] = $breadcrumb; + } + + return $breadcrumbs; + } + + /** + * @return array | null + */ + protected function getNavigationHierarchyBreadcrumbs(): ?array + { + if (filled($cluster = static::getCluster())) { + $clusterBreadcrumbs = $this->getNavigationBreadcrumbs( + $cluster, + static fn (): string => $cluster::getNavigationUrl(), + ); + $pageBreadcrumbs = $this->getNavigationBreadcrumbs( + $this->getSubNavigationBreadcrumbItemKey(), + fn (): ?string => $this->getSubNavigationBreadcrumbItemUrl(), + $this->getCachedSubNavigation(), + ); + + if (($clusterBreadcrumbs === null) || ($pageBreadcrumbs === null)) { + return null; + } + + return [ + ...$clusterBreadcrumbs, + $cluster::getUrl() => $cluster::getClusterBreadcrumb(), + ...$pageBreadcrumbs, + ]; + } + + return $this->getNavigationBreadcrumbs( + $this->getNavigationBreadcrumbItemKey(), + fn (): ?string => $this->getNavigationBreadcrumbItemUrl(), + ); + } + + protected function getNavigationBreadcrumbItemKey(): string + { + return static::class; + } + + protected function getNavigationBreadcrumbItemUrl(): ?string + { + if (! static::shouldRegisterNavigation()) { + return null; + } + + return static::getNavigationUrl(); + } + + protected function getSubNavigationBreadcrumbItemKey(): string + { + return $this->getNavigationBreadcrumbItemKey(); + } + + protected function getSubNavigationBreadcrumbItemUrl(): ?string + { + return $this->getNavigationBreadcrumbItemUrl(); + } + + /** + * @param Closure(): ?string $navigationItemUrl + * @param ?array $navigation + * @return array | null + */ + protected function getNavigationBreadcrumbs(string $navigationItemKey, Closure $navigationItemUrl, ?array $navigation = null): ?array + { + if ($navigation === null) { + $panel = Filament::getCurrentOrDefaultPanel(); + + if (! $panel->hasNavigation()) { + return null; + } + + $navigation = $panel->getNavigation(); + } + + foreach ($navigation as $navigationGroup) { + $breadcrumbs = $this->getNavigationItemBreadcrumbs( + $navigationItemKey, + $navigationItemUrl, + $navigationGroup->getItems(), + ); + + if ($breadcrumbs === null) { + continue; + } + + if (filled($navigationGroupLabel = $navigationGroup->getLabel())) { + $breadcrumbs = [ + $navigationGroupLabel, + ...$breadcrumbs, + ]; + } + + return $breadcrumbs; + } + + return null; + } + + /** + * @param Closure(): ?string $navigationItemUrl + * @param iterable $navigationItems + * @return array | null + */ + protected function getNavigationItemBreadcrumbs(string $navigationItemKey, Closure $navigationItemUrl, iterable $navigationItems): ?array + { + foreach ($navigationItems as $navigationItem) { + if ($navigationItem->getKey() === $navigationItemKey) { + $resolvedNavigationItemUrl = $navigationItemUrl(); + + if (filled($resolvedNavigationItemUrl) && ($navigationItem->getUrl() === $resolvedNavigationItemUrl)) { + return []; + } + } + + $breadcrumbs = $this->getNavigationItemBreadcrumbs( + $navigationItemKey, + $navigationItemUrl, + $navigationItem->getChildItems(), + ); + + if ($breadcrumbs === null) { + continue; + } + + $navigationItemUrl = $navigationItem->getUrl(); + + return [ + ...(filled($navigationItemUrl) + ? [$navigationItemUrl => $navigationItem->getLabel()] + : [$navigationItem->getLabel()]), + ...$breadcrumbs, + ]; + } + + return null; } public static function getNavigationGroup(): string | UnitEnum | null diff --git a/packages/panels/src/Panel/Concerns/HasBreadcrumbs.php b/packages/panels/src/Panel/Concerns/HasBreadcrumbs.php index 541c6ff670..c78cb6d547 100644 --- a/packages/panels/src/Panel/Concerns/HasBreadcrumbs.php +++ b/packages/panels/src/Panel/Concerns/HasBreadcrumbs.php @@ -8,9 +8,12 @@ trait HasBreadcrumbs { protected bool | Closure $hasBreadcrumbs = true; - public function breadcrumbs(bool | Closure $condition = true): static + protected bool | Closure $hasNavigationHierarchyInBreadcrumbs = false; + + public function breadcrumbs(bool | Closure $condition = true, bool | Closure $hasNavigationHierarchy = false): static { $this->hasBreadcrumbs = $condition; + $this->hasNavigationHierarchyInBreadcrumbs = $hasNavigationHierarchy; return $this; } @@ -19,4 +22,9 @@ trait HasBreadcrumbs { return (bool) $this->evaluate($this->hasBreadcrumbs); } + + public function hasNavigationHierarchyInBreadcrumbs(): bool + { + return (bool) $this->evaluate($this->hasNavigationHierarchyInBreadcrumbs); + } } diff --git a/packages/panels/src/Resources/Pages/Page.php b/packages/panels/src/Resources/Pages/Page.php index f9ad8dbf36..54f61047f3 100644 --- a/packages/panels/src/Resources/Pages/Page.php +++ b/packages/panels/src/Resources/Pages/Page.php @@ -40,8 +40,6 @@ abstract class Page extends BasePage use CanAuthorizeResourceAccess; use InteractsWithParentRecord; - protected static ?string $breadcrumb = null; - protected static string $resource; protected static bool $isDiscovered = false; @@ -227,6 +225,17 @@ abstract class Page extends BasePage } } + if (Filament::getCurrentOrDefaultPanel()->hasNavigationHierarchyInBreadcrumbs()) { + $navigationHierarchyBreadcrumbs = $this->getNavigationHierarchyBreadcrumbs(); + + if ($navigationHierarchyBreadcrumbs !== null) { + return [ + ...$navigationHierarchyBreadcrumbs, + ...$breadcrumbs, + ]; + } + } + if (filled($cluster = static::getCluster())) { return $cluster::unshiftClusterBreadcrumbs($breadcrumbs); } @@ -245,6 +254,42 @@ abstract class Page extends BasePage ]; } + protected function getNavigationBreadcrumbItemKey(): string + { + return static::getResource(); + } + + protected function getNavigationBreadcrumbItemUrl(): ?string + { + $resource = static::getResource(); + + if ((! $resource::shouldRegisterNavigation()) || $resource::getParentResourceRegistration()) { + return null; + } + + return $resource::getNavigationUrl(); + } + + protected function getSubNavigationBreadcrumbItemKey(): string + { + return $this->getSubNavigationParameters() ? static::class : parent::getSubNavigationBreadcrumbItemKey(); + } + + protected function getSubNavigationBreadcrumbItemUrl(): ?string + { + $parameters = $this->getSubNavigationParameters(); + + if (! $parameters) { + return parent::getSubNavigationBreadcrumbItemUrl(); + } + + if (! static::shouldRegisterNavigation($parameters)) { + return null; + } + + return static::getNavigationUrl($parameters); + } + /** * @param array $parameters */ diff --git a/tests/src/Panels/Configuration/NavigationTest.php b/tests/src/Panels/Configuration/NavigationTest.php index 8135c1cd9c..5764f98329 100644 --- a/tests/src/Panels/Configuration/NavigationTest.php +++ b/tests/src/Panels/Configuration/NavigationTest.php @@ -4,10 +4,30 @@ use Filament\Facades\Filament; use Filament\Navigation\NavigationItem; use Filament\Tests\Fixtures\Pages\ConfigurableSettings; use Filament\Tests\Fixtures\Resources\Posts\ConfigurablePostResource; +use Filament\Tests\Fixtures\Resources\Posts\Pages\ListConfigurablePosts; use Filament\Tests\Panels\Configuration\TestCase; uses(TestCase::class); +it('includes the navigation hierarchy in breadcrumbs for the active page and resource configurations', function (): void { + Filament::getCurrentOrDefaultPanel()->breadcrumbs(hasNavigationHierarchy: true); + + Filament::forPageConfiguration(ConfigurableSettings::class, 'general'); + + expect(app(ConfigurableSettings::class)->getBreadcrumbs())->toBe([ + 'Settings', + 'Configurable Settings', + ]); + + Filament::setCurrentPageConfigurationKey(null); + Filament::forResourceConfiguration(ConfigurablePostResource::class, 'archived'); + + expect(app(ListConfigurablePosts::class)->getResourceBreadcrumbs())->toBe([ + 'Archive', + ConfigurablePostResource::getUrl() => 'Posts', + ]); +}); + describe('resource configuration navigation', function (): void { it('registers separate navigation items for each resource configuration', function (): void { $navigation = Filament::getNavigation(); diff --git a/tests/src/Panels/Pages/BreadcrumbsTest.php b/tests/src/Panels/Pages/BreadcrumbsTest.php new file mode 100644 index 0000000000..aac1133120 --- /dev/null +++ b/tests/src/Panels/Pages/BreadcrumbsTest.php @@ -0,0 +1,309 @@ +breadcrumbs(false, hasNavigationHierarchy: fn (): bool => true); + + expect($panel->hasBreadcrumbs())->toBeFalse() + ->and($panel->hasNavigationHierarchyInBreadcrumbs())->toBeTrue(); +}); + +it('preserves existing page and resource breadcrumbs when the navigation hierarchy is not included', function (): void { + expect(app(Settings::class)->getBreadcrumbs())->toBe([]) + ->and(app(ListPosts::class)->getResourceBreadcrumbs())->toBe([ + PostResource::getUrl() => 'Posts', + ]); +}); + +it('includes an explicitly configured custom page breadcrumb without the navigation hierarchy', function (): void { + expect(app(PageWithBreadcrumb::class)->getBreadcrumbs())->toBe([ + 'Custom breadcrumb', + ]); +}); + +it('preserves the title fallback for resource page breadcrumbs', function (): void { + expect(app(ResourcePageWithTitle::class)->getBreadcrumb())->toBe('Resource page title'); +}); + +it('includes the navigation hierarchy in breadcrumbs for a grouped page with a parent item', function (): void { + Filament::getCurrentOrDefaultPanel() + ->breadcrumbs(hasNavigationHierarchy: true) + ->navigationItems([ + NavigationItem::make('Administration home') + ->group('Administration') + ->icon(Heroicon::OutlinedHome) + ->url('/administration'), + ...GroupedBreadcrumbsPage::getNavigationItems(), + ]); + + expect(app(GroupedBreadcrumbsPage::class)->getBreadcrumbs())->toBe([ + 'Administration', + '/administration' => 'Administration home', + 'Reports', + ]); + + livewire(GroupedBreadcrumbsPage::class) + ->assertSuccessful(); +}); + +it('includes the navigation hierarchy in breadcrumbs for a grouped resource page with a parent item', function (): void { + PostResource::navigationParentItem('Blog home'); + + Filament::getCurrentOrDefaultPanel() + ->breadcrumbs(hasNavigationHierarchy: true) + ->navigationItems([ + NavigationItem::make('Blog home') + ->group('Blog') + ->icon(Heroicon::OutlinedHome) + ->url('/blog'), + ]); + + expect(app(ListPosts::class)->getResourceBreadcrumbs())->toBe([ + 'Blog', + '/blog' => 'Blog home', + PostResource::getUrl() => 'Posts', + ]); +}); + +it('renders the navigation hierarchy in breadcrumbs', function (): void { + PostResource::navigationParentItem('Blog home'); + + Filament::getCurrentOrDefaultPanel() + ->breadcrumbs(hasNavigationHierarchy: true) + ->navigationItems([ + NavigationItem::make('Blog home') + ->group('Blog') + ->icon(Heroicon::OutlinedHome) + ->url('/blog'), + ]); + + visit(PostResource::getUrl()) + ->assertScript(<<<'JS' + Array.from(document.querySelectorAll('.fi-breadcrumbs-item-label')).map((item) => [ + item.textContent.trim(), + item.href ? new URL(item.href).pathname : null, + ]) + JS, [ + ['Blog', null], + ['Blog home', '/blog'], + ['Posts', '/posts'], + ['List', null], + ]) + ->assertNoAccessibilityIssues(); + + visit(PostResource::getUrl()) + ->inDarkMode() + ->assertNoAccessibilityIssues(); +}); + +it('includes the navigation hierarchy in breadcrumbs for clustered pages', function (): void { + UserManagement::navigationGroup('Administration'); + + Filament::getCurrentOrDefaultPanel() + ->breadcrumbs(hasNavigationHierarchy: true); + + expect(app(ManageAdmins::class)->getBreadcrumbs())->toBe([ + 'Administration', + UserManagement::getUrl() => 'User Management', + 'User Management', + 'Manage Admins', + ]); +}); + +it('includes the navigation hierarchy in breadcrumbs for clustered resource record pages with sub-navigation', function (): void { + UserManagement::navigationGroup('Administration'); + + Filament::getCurrentOrDefaultPanel() + ->breadcrumbs(hasNavigationHierarchy: true); + + expect(app(ClusteredResourceRecordPage::class)->getBreadcrumbs())->toBe([ + 'Administration', + UserManagement::getUrl() => 'User Management', + 'Record', + PostResource::getUrl() => 'Posts', + 'Edit post', + ]); +}); + +it('falls back to cluster breadcrumbs when cluster sub-navigation is disabled', function (): void { + WithoutSubNavigationCluster::navigationGroup('Administration'); + + $breadcrumbs = app(ClusteredPageWithoutSubNavigation::class)->getBreadcrumbs(); + + Filament::getCurrentOrDefaultPanel()->breadcrumbs(hasNavigationHierarchy: true); + + expect(app(ClusteredPageWithoutSubNavigation::class)->getBreadcrumbs())->toBe($breadcrumbs); + + $this->get(ClusteredPageWithoutSubNavigation::getUrl()) + ->assertSuccessful(); +}); + +it('falls back to cluster breadcrumbs when a custom `NavigationBuilder` omits the cluster', function (): void { + $breadcrumbs = app(ManageAdmins::class)->getBreadcrumbs(); + + Filament::getCurrentOrDefaultPanel() + ->breadcrumbs(hasNavigationHierarchy: true) + ->navigation(fn (NavigationBuilder $navigation): NavigationBuilder => $navigation); + + expect(app(ManageAdmins::class)->getBreadcrumbs())->toBe($breadcrumbs); +}); + +it('falls back to existing breadcrumbs when a custom `NavigationBuilder` omits the page hierarchy', function (): void { + $breadcrumbs = app(GroupedBreadcrumbsPage::class)->getBreadcrumbs(); + + Filament::getCurrentOrDefaultPanel() + ->breadcrumbs(hasNavigationHierarchy: true) + ->navigation(fn (NavigationBuilder $navigation): NavigationBuilder => $navigation); + + expect(app(GroupedBreadcrumbsPage::class)->getBreadcrumbs())->toBe($breadcrumbs); +}); + +it('includes the navigation hierarchy in breadcrumbs from a custom `NavigationBuilder`', function (): void { + Filament::getCurrentOrDefaultPanel() + ->breadcrumbs(hasNavigationHierarchy: true) + ->navigation(fn (NavigationBuilder $navigation): NavigationBuilder => $navigation + ->group(NavigationGroup::make('Administration') + ->items(GroupedBreadcrumbsPage::getNavigationItems()))); + + expect(app(GroupedBreadcrumbsPage::class)->getBreadcrumbs())->toBe([ + 'Administration', + 'Reports', + ]); +}); + +it('falls back to existing breadcrumbs for a grouped page that does not register navigation', function (): void { + $breadcrumbs = app(GroupedUnregisteredBreadcrumbsPage::class)->getBreadcrumbs(); + + Filament::getCurrentOrDefaultPanel() + ->breadcrumbs(hasNavigationHierarchy: true) + ->navigationItems([ + NavigationItem::make('Published reports') + ->key(GroupedUnregisteredBreadcrumbsPage::class) + ->group('Administration') + ->icon(Heroicon::OutlinedDocumentText) + ->childItems([ + NavigationItem::make('Published report') + ->url('/published-reports'), + ]), + ]); + + expect(app(GroupedUnregisteredBreadcrumbsPage::class)->getBreadcrumbs())->toBe($breadcrumbs); +}); + +it('falls back to existing breadcrumbs when navigation is disabled', function (): void { + $breadcrumbs = app(GroupedBreadcrumbsPage::class)->getBreadcrumbs(); + + Filament::getCurrentOrDefaultPanel() + ->breadcrumbs(hasNavigationHierarchy: true) + ->navigationItems([ + NavigationItem::make('Administration home') + ->group('Administration') + ->icon(Heroicon::OutlinedHome) + ->url('/administration'), + ...GroupedBreadcrumbsPage::getNavigationItems(), + ]) + ->navigation(false); + + expect(app(GroupedBreadcrumbsPage::class)->getBreadcrumbs())->toBe($breadcrumbs); +}); + +class GroupedBreadcrumbsPage extends Page +{ + protected static string | UnitEnum | null $navigationGroup = 'Administration'; + + protected static ?string $navigationParentItem = 'Administration home'; + + protected static ?string $title = 'Reports'; + + protected string $view = 'filament-panels::pages.page'; + + public static function getNavigationUrl(): string + { + return '/reports'; + } +} + +class PageWithBreadcrumb extends Page +{ + protected static ?string $breadcrumb = 'Custom breadcrumb'; + + protected string $view = 'filament-panels::pages.page'; +} + +class ResourcePageWithTitle extends ResourcePage +{ + protected static string $resource = PostResource::class; + + protected static ?string $title = 'Resource page title'; +} + +class ClusteredResourceRecordPage extends ResourcePage +{ + protected static string $resource = PostResource::class; + + protected static ?string $title = 'Edit post'; + + public static function getCluster(): ?string + { + return UserManagement::class; + } + + public function getSubNavigation(): array + { + return [ + NavigationItem::make('Edit post') + ->key(static::class) + ->group('Record') + ->url('/posts/1/edit'), + ]; + } + + public function getSubNavigationParameters(): array + { + return ['record' => 1]; + } + + public static function getNavigationUrl(array $parameters = []): string + { + return "/posts/{$parameters['record']}/edit"; + } +} + +class GroupedUnregisteredBreadcrumbsPage extends Page +{ + protected static string | UnitEnum | null $navigationGroup = 'Administration'; + + protected static bool $shouldRegisterNavigation = false; + + protected static ?string $title = 'Archived reports'; + + protected string $view = 'filament-panels::pages.page'; +} diff --git a/tests/src/Panels/Resources/NestedResourceTest.php b/tests/src/Panels/Resources/NestedResourceTest.php index c31d705327..3ab32e6af8 100644 --- a/tests/src/Panels/Resources/NestedResourceTest.php +++ b/tests/src/Panels/Resources/NestedResourceTest.php @@ -2,6 +2,7 @@ use Filament\Actions\DeleteAction; use Filament\Actions\DeleteBulkAction; +use Filament\Facades\Filament; use Filament\Tests\Fixtures\Models\Company; use Filament\Tests\Fixtures\Models\Post; use Filament\Tests\Fixtures\Models\Team; @@ -25,6 +26,22 @@ use function Pest\Laravel\assertSoftDeleted; uses(TestCase::class); describe('soft-deletable nested resource', function (): void { + it('can render with the navigation hierarchy in breadcrumbs regardless of panel navigation visibility', function (): void { + $parentRecord = User::factory()->create(); + $url = UserPostResource::getUrl('index', [ + 'author' => $parentRecord, + ]); + + $panel = Filament::getCurrentOrDefaultPanel() + ->breadcrumbs(hasNavigationHierarchy: true); + + $this->get($url)->assertSuccessful(); + + $panel->navigation(false); + + $this->get($url)->assertSuccessful(); + }); + it('can render list page', function (): void { $parentRecord = User::factory()->create();