From 8ab37e9c8b4e52f5a77608a128270f99167f3861 Mon Sep 17 00:00:00 2001 From: Manuk Date: Wed, 12 Aug 2026 01:24:59 +0400 Subject: [PATCH 1/2] feat: allow the host app to register its own management page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filament reads sub-navigation, breadcrumbs and header actions off the page class, none of which the `management` config block can reach. Consumers that want the custom fields screen inside their own settings navigation had no option but to subclass and live with the packaged page still registered on the panel — a second, unlinked route to the same screen. `CustomFieldsPlugin::managementPage()` swaps the registered page instead of adding to it, so there is exactly one route either way. The class must extend `CustomFieldsManagementPage`, so all packaged behaviour is inherited by default. --- docs/content/2.essentials/1.configuration.md | 40 +++++++++++++++++++ src/CustomFieldsPlugin.php | 37 ++++++++++++++++- tests/Feature/ManagementPageOverrideTest.php | 40 +++++++++++++++++++ tests/Fixtures/Pages/CustomManagementPage.php | 16 ++++++++ 4 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 tests/Feature/ManagementPageOverrideTest.php create mode 100644 tests/Fixtures/Pages/CustomManagementPage.php diff --git a/docs/content/2.essentials/1.configuration.md b/docs/content/2.essentials/1.configuration.md index 1bec9ff9..c235c00b 100644 --- a/docs/content/2.essentials/1.configuration.md +++ b/docs/content/2.essentials/1.configuration.md @@ -135,6 +135,46 @@ Configure the custom fields management page: ], ``` +#### Replacing the Management Page + +The config above covers the common cases. Filament reads some things off the page +class itself rather than from config — sub-navigation, breadcrumbs, header actions — +so when you need one of those, register your own page instead: + +```php +use App\Filament\Pages\Settings\CustomFields; + +CustomFieldsPlugin::make() + ->managementPage(CustomFields::class) +``` + +```php +namespace App\Filament\Pages\Settings; + +use Filament\Panel; +use Filament\Pages\Enums\SubNavigationPosition; +use Relaticle\CustomFields\Filament\Management\Pages\CustomFieldsManagementPage; + +class CustomFields extends CustomFieldsManagementPage +{ + protected static ?SubNavigationPosition $subNavigationPosition = SubNavigationPosition::Start; + + public static function getSlug(?Panel $panel = null): string + { + return 'settings/custom-fields'; + } + + public function getSubNavigation(): array + { + // Render this page inside your own settings navigation. + } +} +``` + +The page must extend `CustomFieldsManagementPage`, and it replaces the packaged page +rather than sitting alongside it — only one management page is registered on the panel, +so there is no second route to the same screen. + ### Database Configuration Customize table names and paths: diff --git a/src/CustomFieldsPlugin.php b/src/CustomFieldsPlugin.php index 1102b0e2..e94a0c95 100644 --- a/src/CustomFieldsPlugin.php +++ b/src/CustomFieldsPlugin.php @@ -10,6 +10,7 @@ use Filament\Panel; use Filament\Support\Concerns\EvaluatesClosures; use Filament\Support\Enums\Width; +use InvalidArgumentException; use Relaticle\CustomFields\Enums\CustomFieldsFeature; use Relaticle\CustomFields\Facades\CustomFieldsType; use Relaticle\CustomFields\FeatureSystem\FeatureManager; @@ -29,6 +30,9 @@ class CustomFieldsPlugin implements Plugin protected Width|Closure|null $sectionModalWidth = null; + /** @var class-string|null */ + protected ?string $managementPage = null; + public function getId(): string { return 'custom-fields'; @@ -38,7 +42,7 @@ public function register(Panel $panel): void { $panel ->pages([ - CustomFieldsManagementPage::class, + $this->getManagementPage(), ]) ->tenantMiddleware([SetTenantContextMiddleware::class], true); } @@ -119,6 +123,37 @@ public function sectionModalWidth(Width|Closure|null $width): static return $this; } + /** + * Register your own management page in place of the packaged one, so the host + * application controls the things Filament reads off the page class itself — + * slug, sub-navigation, cluster, heading. The subclass keeps all packaged + * behaviour; only the page registered on the panel changes. + * + * @param class-string $page + */ + public function managementPage(string $page): static + { + if (! is_subclass_of($page, CustomFieldsManagementPage::class)) { + throw new InvalidArgumentException(sprintf( + '[%s] must extend [%s] to be used as the management page.', + $page, + CustomFieldsManagementPage::class, + )); + } + + $this->managementPage = $page; + + return $this; + } + + /** + * @return class-string + */ + public function getManagementPage(): string + { + return $this->managementPage ?? CustomFieldsManagementPage::class; + } + public function getSectionModalWidth(): Width { $width = $this->evaluate($this->sectionModalWidth); diff --git a/tests/Feature/ManagementPageOverrideTest.php b/tests/Feature/ManagementPageOverrideTest.php new file mode 100644 index 00000000..a3d9ea82 --- /dev/null +++ b/tests/Feature/ManagementPageOverrideTest.php @@ -0,0 +1,40 @@ +getManagementPage()) + ->toBe(CustomFieldsManagementPage::class); +}); + +it('registers an overridden management page', function (): void { + expect(CustomFieldsPlugin::make()->managementPage(CustomManagementPage::class)->getManagementPage()) + ->toBe(CustomManagementPage::class); +}); + +it('rejects a management page that does not extend the packaged one', function (): void { + CustomFieldsPlugin::make()->managementPage(Panel::class); +})->throws(InvalidArgumentException::class, 'must extend'); + +it('puts only the overridden page on the panel', function (): void { + $panel = Panel::make(); + + CustomFieldsPlugin::make()->managementPage(CustomManagementPage::class)->register($panel); + + expect($panel->getPages()) + ->toContain(CustomManagementPage::class) + ->not->toContain(CustomFieldsManagementPage::class); +}); + +it('puts the packaged page on the panel when not overridden', function (): void { + $panel = Panel::make(); + + CustomFieldsPlugin::make()->register($panel); + + expect($panel->getPages())->toContain(CustomFieldsManagementPage::class); +}); diff --git a/tests/Fixtures/Pages/CustomManagementPage.php b/tests/Fixtures/Pages/CustomManagementPage.php new file mode 100644 index 00000000..211d095c --- /dev/null +++ b/tests/Fixtures/Pages/CustomManagementPage.php @@ -0,0 +1,16 @@ + Date: Wed, 12 Aug 2026 22:08:09 +0400 Subject: [PATCH 2/2] fix: accept the packaged page itself in managementPage() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit is_subclass_of() is false for the class itself, so passing CustomFieldsManagementPage::class — a legitimate explicit 'use the default' — threw. is_a(..., allow_string: true) accepts the class and its subclasses. --- src/CustomFieldsPlugin.php | 4 +++- tests/Feature/ManagementPageOverrideTest.php | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/CustomFieldsPlugin.php b/src/CustomFieldsPlugin.php index e94a0c95..13700e16 100644 --- a/src/CustomFieldsPlugin.php +++ b/src/CustomFieldsPlugin.php @@ -133,7 +133,9 @@ public function sectionModalWidth(Width|Closure|null $width): static */ public function managementPage(string $page): static { - if (! is_subclass_of($page, CustomFieldsManagementPage::class)) { + // is_a() rather than is_subclass_of(), so passing the packaged page + // itself is an explicit no-op rather than an error. + if (! is_a($page, CustomFieldsManagementPage::class, allow_string: true)) { throw new InvalidArgumentException(sprintf( '[%s] must extend [%s] to be used as the management page.', $page, diff --git a/tests/Feature/ManagementPageOverrideTest.php b/tests/Feature/ManagementPageOverrideTest.php index a3d9ea82..bf999a88 100644 --- a/tests/Feature/ManagementPageOverrideTest.php +++ b/tests/Feature/ManagementPageOverrideTest.php @@ -17,6 +17,11 @@ ->toBe(CustomManagementPage::class); }); +it('accepts the packaged page itself as an explicit no-op', function (): void { + expect(CustomFieldsPlugin::make()->managementPage(CustomFieldsManagementPage::class)->getManagementPage()) + ->toBe(CustomFieldsManagementPage::class); +}); + it('rejects a management page that does not extend the packaged one', function (): void { CustomFieldsPlugin::make()->managementPage(Panel::class); })->throws(InvalidArgumentException::class, 'must extend');