From c935ade23762861c040b59a41371edf799f77615 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 14 Oct 2026 19:38:31 +0000 Subject: [PATCH] Fix signed-in help links for articles that are not public. Share uses the panel that registers the guest or signed-in help resource. Signed-in help pages include articles that are not public. Hidden articles and the guest page stay unavailable. --- README.md | 7 +- src/FilamentHelpFrontendPlugin.php | 5 +- src/FilamentHelpGuestPlugin.php | 5 +- src/FilamentHelpPlugin.php | 3 +- .../Frontend/HelpArticleResource.php | 1 - .../Frontend/Pages/ViewHelpArticle.php | 9 +- .../Pages/ViewHelpArticle.php | 33 ++++--- .../Feature/AdminHelpArticleResourceTest.php | 3 +- tests/Feature/FrontendHelpTest.php | 4 +- tests/Feature/HelpArticleVisibilityTest.php | 90 +++++++++++++++++++ tests/TestCase.php | 33 +++++-- 11 files changed, 157 insertions(+), 36 deletions(-) create mode 100644 tests/Feature/HelpArticleVisibilityTest.php diff --git a/README.md b/README.md index 3d0db8c2..ad05fa8f 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,7 @@ public function panel(Panel $panel): Panel **Location**: App panel (defaults to `{panel-path}/help-articles`, configurable via `->slug()`) **Access**: Authenticated users only -**Features**: Read-only access to public help articles +**Features**: Read-only access to help articles that are not hidden, including articles without public access ### Guest Plugin (for public access) @@ -257,7 +257,7 @@ public function panel(Panel $panel): Panel Help articles are available in three different locations depending on your setup: 1. **Admin Panel** (`/admin/help-articles`): For editing and managing help articles -2. **App Panel** (configurable, default `{panel-path}/help-articles`): For authenticated users to view public help articles +2. **App Panel** (configurable, default `{panel-path}/help-articles`): For authenticated users to view help articles that are not hidden 3. **Guest Panel** (configurable, default `{panel-path}/help`): For public/guest users to view public help articles The frontend and guest panel URLs can be customized using the plugin's `->slug()` method when registering the plugin (see plugin documentation above). @@ -265,7 +265,8 @@ The frontend and guest panel URLs can be customized using the plugin's `->slug() ## Features - **Admin Panel**: Full CRUD operations for help articles -- **Frontend/Guest**: Read-only access to public help articles +- **Frontend**: Read-only access for authenticated users, including articles that are not public +- **Guest**: Read-only access to public help articles - **Rich Content**: HTML content support with iframe embedding - **Public/Private**: Control article visibility with `is_public` flag - **Hidden/Draft**: Hide articles from public view with `is_hidden` flag (useful for drafts or archived articles) diff --git a/src/FilamentHelpFrontendPlugin.php b/src/FilamentHelpFrontendPlugin.php index b886d8bf..a0727d5c 100644 --- a/src/FilamentHelpFrontendPlugin.php +++ b/src/FilamentHelpFrontendPlugin.php @@ -4,6 +4,7 @@ use Filament\Contracts\Plugin; use Filament\Panel; +use Tapp\FilamentHelp\Resources\Frontend\HelpArticleResource; class FilamentHelpFrontendPlugin implements Plugin { @@ -36,11 +37,11 @@ public function register(Panel $panel): void throw new \InvalidArgumentException('Slug cannot be empty. Please provide a non-empty slug when registering the plugin.'); } - \Tapp\FilamentHelp\Resources\Frontend\HelpArticleResource::setSlug($slug); + HelpArticleResource::setSlug($slug); $panel ->resources([ - \Tapp\FilamentHelp\Resources\Frontend\HelpArticleResource::class, + HelpArticleResource::class, ]); } diff --git a/src/FilamentHelpGuestPlugin.php b/src/FilamentHelpGuestPlugin.php index c924081f..de943c40 100644 --- a/src/FilamentHelpGuestPlugin.php +++ b/src/FilamentHelpGuestPlugin.php @@ -4,6 +4,7 @@ use Filament\Contracts\Plugin; use Filament\Panel; +use Tapp\FilamentHelp\Resources\Guest\HelpArticleResource; class FilamentHelpGuestPlugin implements Plugin { @@ -38,11 +39,11 @@ public function register(Panel $panel): void throw new \InvalidArgumentException('Default slug cannot be empty. Please provide a non-empty slug when registering the plugin.'); } - \Tapp\FilamentHelp\Resources\Guest\HelpArticleResource::setSlug($slug); + HelpArticleResource::setSlug($slug); $panel ->resources([ - \Tapp\FilamentHelp\Resources\Guest\HelpArticleResource::class, + HelpArticleResource::class, ]); } diff --git a/src/FilamentHelpPlugin.php b/src/FilamentHelpPlugin.php index 3f7a80e5..84da6de1 100644 --- a/src/FilamentHelpPlugin.php +++ b/src/FilamentHelpPlugin.php @@ -4,6 +4,7 @@ use Filament\Contracts\Plugin; use Filament\Panel; +use Tapp\FilamentHelp\Resources\HelpArticleResource; class FilamentHelpPlugin implements Plugin { @@ -21,7 +22,7 @@ public function register(Panel $panel): void { $panel ->resources([ - \Tapp\FilamentHelp\Resources\HelpArticleResource::class, + HelpArticleResource::class, ]); } diff --git a/src/Resources/Frontend/HelpArticleResource.php b/src/Resources/Frontend/HelpArticleResource.php index baa9d785..19aa1bdf 100644 --- a/src/Resources/Frontend/HelpArticleResource.php +++ b/src/Resources/Frontend/HelpArticleResource.php @@ -103,7 +103,6 @@ public static function canDelete($record): bool public static function getEloquentQuery(): Builder { $query = parent::getEloquentQuery() - ->public() ->visible(); // Apply tenant scoping if enabled diff --git a/src/Resources/Frontend/Pages/ViewHelpArticle.php b/src/Resources/Frontend/Pages/ViewHelpArticle.php index cb873812..cef242a6 100644 --- a/src/Resources/Frontend/Pages/ViewHelpArticle.php +++ b/src/Resources/Frontend/Pages/ViewHelpArticle.php @@ -13,12 +13,11 @@ class ViewHelpArticle extends ViewRecord protected string $view = 'filament-help::resources.frontend.help-article-resource.pages.view-help-article'; - public function mount(int | string $record): void + public function mount(int|string $record): void { $this->record = $this->resolveRecord($record); - - // Ensure the article is public - if (! $this->record->is_public) { + + if ($this->record->is_hidden) { abort(404); } } @@ -35,7 +34,7 @@ public function getHeading(): string public function getSubheading(): string { - return 'Last updated ' . $this->record->updated_at->format('M j, Y'); + return 'Last updated '.$this->record->updated_at->format('M j, Y'); } public function getBreadcrumbs(): array diff --git a/src/Resources/HelpArticleResource/Pages/ViewHelpArticle.php b/src/Resources/HelpArticleResource/Pages/ViewHelpArticle.php index 3ce17c7f..7c9843e5 100644 --- a/src/Resources/HelpArticleResource/Pages/ViewHelpArticle.php +++ b/src/Resources/HelpArticleResource/Pages/ViewHelpArticle.php @@ -54,20 +54,31 @@ protected function getShareUrl(): string $record = $this->record; if ($record->is_public) { - // Public link for unauthenticated users (guest panel) - $guestPanel = Filament::getPanel('guest'); - if ($guestPanel) { - // Temporarily set the current panel to generate the correct URL - $originalPanel = Filament::getCurrentPanel(); - Filament::setCurrentPanel($guestPanel); - $url = GuestHelpArticleResource::getUrl('view', ['record' => $record->slug]); - Filament::setCurrentPanel($originalPanel); + $guestPanelId = $this->panelIdForResource(GuestHelpArticleResource::class); - return $url; + if (filled($guestPanelId)) { + return GuestHelpArticleResource::getUrl('view', ['record' => $record->slug], panel: $guestPanelId); } } - // Authenticated frontend link (not admin) - return FrontendHelpArticleResource::getUrl('view', ['record' => $record->slug]); + return FrontendHelpArticleResource::getUrl( + 'view', + ['record' => $record->slug], + panel: $this->panelIdForResource(FrontendHelpArticleResource::class), + ); + } + + /** + * @param class-string $resource + */ + protected function panelIdForResource(string $resource): ?string + { + foreach (Filament::getPanels() as $panel) { + if (in_array($resource, $panel->getResources(), true)) { + return $panel->getId(); + } + } + + return null; } } diff --git a/tests/Feature/AdminHelpArticleResourceTest.php b/tests/Feature/AdminHelpArticleResourceTest.php index 469f8acd..40658bb6 100644 --- a/tests/Feature/AdminHelpArticleResourceTest.php +++ b/tests/Feature/AdminHelpArticleResourceTest.php @@ -4,6 +4,7 @@ use Filament\Schemas\Schema; use Tapp\FilamentHelp\Models\HelpArticle; use Tapp\FilamentHelp\Resources\HelpArticleResource; +use Tapp\FilamentHelp\Resources\HelpArticleResource\Pages\ViewHelpArticle; it('can create help article through factory', function () { $helpArticle = HelpArticle::factory()->create([ @@ -49,7 +50,7 @@ 'name' => 'Adding an Evaluation Course', ]); - $page = new \Tapp\FilamentHelp\Resources\HelpArticleResource\Pages\ViewHelpArticle; + $page = new ViewHelpArticle; $page->record = $helpArticle; expect($page->getTitle())->toBe('Adding an Evaluation Course'); diff --git a/tests/Feature/FrontendHelpTest.php b/tests/Feature/FrontendHelpTest.php index 8a3a0495..a6f4fc98 100644 --- a/tests/Feature/FrontendHelpTest.php +++ b/tests/Feature/FrontendHelpTest.php @@ -26,7 +26,7 @@ $originalSlug = $helpArticle->slug; $helpArticle->update(['name' => 'Updated Name']); - + expect($helpArticle->slug)->not->toBe($originalSlug); expect($helpArticle->slug)->toBe('updated-name'); }); @@ -38,7 +38,7 @@ ]); $foundArticle = HelpArticle::where('slug', 'test-article')->first(); - + expect($foundArticle)->not->toBeNull(); expect($foundArticle->name)->toBe('Test Article'); }); diff --git a/tests/Feature/HelpArticleVisibilityTest.php b/tests/Feature/HelpArticleVisibilityTest.php new file mode 100644 index 00000000..527b9209 --- /dev/null +++ b/tests/Feature/HelpArticleVisibilityTest.php @@ -0,0 +1,90 @@ +private()->create([ + 'name' => 'Private policies', + 'slug' => 'private-policies', + ]); + + expect(Route::has('filament.admin.resources.help.view'))->toBeFalse(); + expect(shareUrl($article))->toBe(route('filament.app.resources.help.view', ['record' => 'private-policies'])); +}); + +it('shares a public article on the guest panel while admin is current', function () { + $article = HelpArticle::factory()->public()->create([ + 'name' => 'Public guide', + 'slug' => 'public-guide', + ]); + + expect(shareUrl($article))->toBe(route('filament.guest.resources.help-articles.view', ['record' => 'public-guide'])); +}); + +it('lists private articles for the signed-in help page and hides drafts', function () { + HelpArticle::factory()->private()->create(['slug' => 'private-policies']); + HelpArticle::factory()->public()->create(['slug' => 'public-guide']); + HelpArticle::factory()->private()->hidden()->create(['slug' => 'draft-article']); + + Filament::setCurrentPanel(Filament::getPanel('app')); + + $slugs = FrontendHelpArticleResource::getEloquentQuery()->pluck('slug'); + + expect($slugs->all())->toContain('private-policies', 'public-guide') + ->not->toContain('draft-article'); +}); + +it('opens a private article on the signed-in help page', function () { + $article = HelpArticle::factory()->private()->create([ + 'name' => 'Private policies', + 'slug' => 'private-policies', + ]); + + Filament::setCurrentPanel(Filament::getPanel('app')); + + $page = new FrontendViewHelpArticle; + $page->mount($article->slug); + + expect($page->getRecord()->is($article))->toBeTrue() + ->and($page->getTitle())->toBe('Private policies'); +}); + +it('does not open a hidden article on the signed-in help page', function () { + $article = HelpArticle::factory()->hidden()->create([ + 'slug' => 'draft-article', + ]); + + $this->get(route('filament.app.resources.help.view', ['record' => $article->slug])) + ->assertNotFound(); +}); + +it('does not open a private article on the guest help page', function () { + $article = HelpArticle::factory()->private()->create([ + 'slug' => 'private-policies', + ]); + + expect(GuestHelpArticleResource::getEloquentQuery()->pluck('slug')->all())->not->toContain('private-policies'); + + $this->get(route('filament.guest.resources.help-articles.view', ['record' => $article->slug])) + ->assertNotFound(); +}); + +function shareUrl(HelpArticle $article): string +{ + $page = new ViewHelpArticle; + $page->record = $article; + + $method = new ReflectionMethod(ViewHelpArticle::class, 'getShareUrl'); + + return $method->invoke($page); +} diff --git a/tests/TestCase.php b/tests/TestCase.php index dade4b47..0263a068 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,11 +2,16 @@ namespace Tapp\FilamentHelp\Tests; +use Filament\Actions\ActionsServiceProvider; +use Filament\Facades\Filament; use Filament\FilamentServiceProvider; +use Filament\Panel; +use Filament\Support\SupportServiceProvider; use Illuminate\Database\Eloquent\Factories\Factory; use Livewire\LivewireServiceProvider; use Orchestra\Testbench\TestCase as Orchestra; use Tapp\FilamentHelp\FilamentHelpServiceProvider; +use Tapp\FilamentHelp\Resources\HelpArticleResource; class TestCase extends Orchestra { @@ -24,8 +29,8 @@ protected function getPackageProviders($app) return [ LivewireServiceProvider::class, FilamentServiceProvider::class, - \Filament\Actions\ActionsServiceProvider::class, - \Filament\Support\SupportServiceProvider::class, + ActionsServiceProvider::class, + SupportServiceProvider::class, FilamentHelpServiceProvider::class, ]; } @@ -37,27 +42,39 @@ public function getEnvironmentSetUp($app) $migration = include __DIR__.'/../database/migrations/create_help_articles_table.php.stub'; $migration->up(); - + $addIsHiddenMigration = include __DIR__.'/../database/migrations/add_is_hidden_to_help_articles_table.php.stub'; $addIsHiddenMigration->up(); + // Match an app that puts the signed-in help page on `help` and the guest page on `help-articles`. + \Tapp\FilamentHelp\Resources\Frontend\HelpArticleResource::setSlug('help'); + \Tapp\FilamentHelp\Resources\Guest\HelpArticleResource::setSlug('help-articles'); + // Set up a basic Filament panel for testing (admin) - \Filament\Facades\Filament::registerPanel( - \Filament\Panel::make() + Filament::registerPanel( + Panel::make() ->id('admin') ->resources([ - \Tapp\FilamentHelp\Resources\HelpArticleResource::class, + HelpArticleResource::class, ]) ); // Register the frontend panel for testing (app) - \Filament\Facades\Filament::registerPanel( - \Filament\Panel::make() + Filament::registerPanel( + Panel::make() ->id('app') ->default() // Make 'app' the default panel for frontend tests ->resources([ \Tapp\FilamentHelp\Resources\Frontend\HelpArticleResource::class, ]) ); + + Filament::registerPanel( + Panel::make() + ->id('guest') + ->resources([ + \Tapp\FilamentHelp\Resources\Guest\HelpArticleResource::class, + ]) + ); } }