diff --git a/app/Filament/Resources/PluginResource/Pages/EditPlugin.php b/app/Filament/Resources/PluginResource/Pages/EditPlugin.php index c8f40df0..f2ab2a1c 100644 --- a/app/Filament/Resources/PluginResource/Pages/EditPlugin.php +++ b/app/Filament/Resources/PluginResource/Pages/EditPlugin.php @@ -161,7 +161,7 @@ protected function getHeaderActions(): array ->color('gray') ->url(fn () => route('plugins.show', $this->record->routeParams())) ->openUrlInNewTab() - ->visible(fn () => $this->record->isApproved()), + ->visible(fn () => $this->record->isApproved() || $this->record->isPending()), Actions\Action::make('viewPackagist') ->label('View on Packagist') diff --git a/app/Filament/Resources/PluginResource/Pages/ViewPlugin.php b/app/Filament/Resources/PluginResource/Pages/ViewPlugin.php index 3be53785..600f29e6 100644 --- a/app/Filament/Resources/PluginResource/Pages/ViewPlugin.php +++ b/app/Filament/Resources/PluginResource/Pages/ViewPlugin.php @@ -20,7 +20,7 @@ protected function getHeaderActions(): array ->color('gray') ->url(fn () => route('plugins.show', $this->record->routeParams())) ->openUrlInNewTab() - ->visible(fn () => $this->record->isApproved()), + ->visible(fn () => $this->record->isApproved() || $this->record->isPending()), Actions\Action::make('approve') ->icon('heroicon-o-check') diff --git a/app/Http/Controllers/PluginDirectoryController.php b/app/Http/Controllers/PluginDirectoryController.php index e2e86b52..4b0855aa 100644 --- a/app/Http/Controllers/PluginDirectoryController.php +++ b/app/Http/Controllers/PluginDirectoryController.php @@ -49,12 +49,14 @@ public function show(string $vendor, string $package): View { $plugin = Plugin::findByVendorPackageOrFail($vendor, $package); - abort_unless($plugin->isApproved(), 404); - $user = Auth::user(); - // For paid plugins, check if user has an accessible price - if ($plugin->isPaid() && ! $plugin->hasAccessiblePriceFor($user)) { + $isAdmin = $user?->isAdmin() ?? false; + + abort_unless($plugin->isApproved() || $isAdmin, 404); + + // For paid plugins, check if user has an accessible price (admins bypass) + if (! $isAdmin && $plugin->isPaid() && ! $plugin->hasAccessiblePriceFor($user)) { abort(404); } @@ -72,6 +74,7 @@ public function show(string $vendor, string $package): View 'bestPrice' => $bestPrice, 'regularPrice' => $regularPrice, 'hasDiscount' => $bestPrice && $regularPrice && $bestPrice->id !== $regularPrice->id, + 'isAdminPreview' => ! $plugin->isApproved(), ]); } diff --git a/package-lock.json b/package-lock.json index abb3f776..21461018 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "nativephp", + "name": "kind-goose", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/resources/views/components/plugin-toc.blade.php b/resources/views/components/plugin-toc.blade.php new file mode 100644 index 00000000..ad14bdf3 --- /dev/null +++ b/resources/views/components/plugin-toc.blade.php @@ -0,0 +1,35 @@ +
+

+ +
On this page
+

+ +
+ +
+
diff --git a/resources/views/plugin-license.blade.php b/resources/views/plugin-license.blade.php index 55c3f687..91d9c589 100644 --- a/resources/views/plugin-license.blade.php +++ b/resources/views/plugin-license.blade.php @@ -1,6 +1,6 @@
diff --git a/resources/views/plugin-show.blade.php b/resources/views/plugin-show.blade.php index a0963e4f..8bfd2888 100644 --- a/resources/views/plugin-show.blade.php +++ b/resources/views/plugin-show.blade.php @@ -3,6 +3,14 @@ class="mx-auto mt-10 w-full max-w-7xl" aria-labelledby="plugin-title" > + @if ($isAdminPreview ?? false) +
+

+ Admin Preview — This plugin is not yet published. Status: {{ $plugin->status->label() }} +

+
+ @endif +
{{-- Blurred circle - Decorative --}}
{{-- Main content - README --}} -
+
@if ($plugin->readme_html) - {!! $plugin->readme_html !!} + + @endif + +
+ @if ($plugin->readme_html) + {!! $plugin->readme_html !!} @else

README not available yet.

- @endif -
+ @endif +
+
{{-- Sidebar - Plugin details --}}
diff --git a/tests/Feature/AdminPluginPreviewTest.php b/tests/Feature/AdminPluginPreviewTest.php new file mode 100644 index 00000000..40f5d2a9 --- /dev/null +++ b/tests/Feature/AdminPluginPreviewTest.php @@ -0,0 +1,86 @@ +pending()->create(); + + $this->get(route('plugins.show', $plugin->routeParams())) + ->assertStatus(404); + } + + public function test_regular_user_cannot_view_pending_plugin(): void + { + $user = User::factory()->create(); + $plugin = Plugin::factory()->pending()->create(); + + $this->actingAs($user) + ->get(route('plugins.show', $plugin->routeParams())) + ->assertStatus(404); + } + + public function test_admin_can_view_pending_plugin(): void + { + $admin = User::factory()->create(['email' => 'admin@test.com']); + config(['filament.users' => ['admin@test.com']]); + + $plugin = Plugin::factory()->pending()->create(); + + $this->actingAs($admin) + ->get(route('plugins.show', $plugin->routeParams())) + ->assertStatus(200); + } + + public function test_admin_sees_preview_banner_on_pending_plugin(): void + { + $admin = User::factory()->create(['email' => 'admin@test.com']); + config(['filament.users' => ['admin@test.com']]); + + $plugin = Plugin::factory()->pending()->create(); + + $this->actingAs($admin) + ->get(route('plugins.show', $plugin->routeParams())) + ->assertSee('Admin Preview') + ->assertSee('Pending Review'); + } + + public function test_approved_plugin_does_not_show_preview_banner(): void + { + $plugin = Plugin::factory()->approved()->create(); + + $this->get(route('plugins.show', $plugin->routeParams())) + ->assertDontSee('Admin Preview'); + } + + public function test_admin_can_view_approved_plugin_without_preview_banner(): void + { + $admin = User::factory()->create(['email' => 'admin@test.com']); + config(['filament.users' => ['admin@test.com']]); + + $plugin = Plugin::factory()->approved()->create(); + + $this->actingAs($admin) + ->get(route('plugins.show', $plugin->routeParams())) + ->assertStatus(200) + ->assertDontSee('Admin Preview'); + } +} diff --git a/tests/Feature/PluginTableOfContentsTest.php b/tests/Feature/PluginTableOfContentsTest.php new file mode 100644 index 00000000..73566de5 --- /dev/null +++ b/tests/Feature/PluginTableOfContentsTest.php @@ -0,0 +1,43 @@ +approved()->create([ + 'readme_html' => '

Installation

Steps here.

Usage

More content.

', + ]); + + $this->get(route('plugins.show', $plugin->routeParams())) + ->assertStatus(200) + ->assertSee('On this page'); + } + + public function test_toc_component_not_rendered_when_no_readme(): void + { + $plugin = Plugin::factory()->approved()->create([ + 'readme_html' => null, + ]); + + $this->get(route('plugins.show', $plugin->routeParams())) + ->assertStatus(200) + ->assertDontSee('On this page'); + } +}