Skip to content

Commit 8c83064

Browse files
simonhampclaude
andcommitted
Add admin preview for plugin listings, table of contents, and narrow license page
- Allow admins to preview plugin listing pages before approval, with an amber preview banner showing the current status - Make "View Listing Page" Filament action visible for pending plugins - Add Alpine.js-powered table of contents to plugin listing sidebar, extracted from readme headings - Narrow the plugin license page layout to match the sponsor page (max-w-3xl) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e77aa06 commit 8c83064

File tree

9 files changed

+184
-6
lines changed

9 files changed

+184
-6
lines changed

app/Filament/Resources/PluginResource/Pages/EditPlugin.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ protected function getHeaderActions(): array
161161
->color('gray')
162162
->url(fn () => route('plugins.show', $this->record->routeParams()))
163163
->openUrlInNewTab()
164-
->visible(fn () => $this->record->isApproved()),
164+
->visible(fn () => $this->record->isApproved() || $this->record->isPending()),
165165

166166
Actions\Action::make('viewPackagist')
167167
->label('View on Packagist')

app/Filament/Resources/PluginResource/Pages/ViewPlugin.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ protected function getHeaderActions(): array
2020
->color('gray')
2121
->url(fn () => route('plugins.show', $this->record->routeParams()))
2222
->openUrlInNewTab()
23-
->visible(fn () => $this->record->isApproved()),
23+
->visible(fn () => $this->record->isApproved() || $this->record->isPending()),
2424

2525
Actions\Action::make('approve')
2626
->icon('heroicon-o-check')

app/Http/Controllers/PluginDirectoryController.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ public function show(string $vendor, string $package): View
4949
{
5050
$plugin = Plugin::findByVendorPackageOrFail($vendor, $package);
5151

52-
abort_unless($plugin->isApproved(), 404);
53-
5452
$user = Auth::user();
5553

54+
abort_unless($plugin->isApproved() || $user?->isAdmin(), 404);
55+
5656
// For paid plugins, check if user has an accessible price
5757
if ($plugin->isPaid() && ! $plugin->hasAccessiblePriceFor($user)) {
5858
abort(404);
@@ -72,6 +72,7 @@ public function show(string $vendor, string $package): View
7272
'bestPrice' => $bestPrice,
7373
'regularPrice' => $regularPrice,
7474
'hasDiscount' => $bestPrice && $regularPrice && $bestPrice->id !== $regularPrice->id,
75+
'isAdminPreview' => ! $plugin->isApproved(),
7576
]);
7677
}
7778

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<div
2+
x-data="{
3+
headings: [],
4+
init() {
5+
const article = document.querySelector('article')
6+
if (! article) return
7+
8+
const elements = article.querySelectorAll('h2[id], h3[id]')
9+
this.headings = Array.from(elements).map(el => ({
10+
id: el.id,
11+
text: el.textContent.replace(/^#\s*/, '').trim(),
12+
level: parseInt(el.tagName.substring(1)),
13+
}))
14+
},
15+
}"
16+
x-show="headings.length > 0"
17+
x-cloak
18+
class="mb-4 rounded-2xl p-6"
19+
>
20+
<h2 class="flex items-center gap-1.5 text-sm opacity-60">
21+
<x-icons.stacked-lines class="size-[18px]" />
22+
<div>On this page</div>
23+
</h2>
24+
25+
<nav class="mt-4 flex flex-col space-y-2 overflow-y-auto overflow-x-hidden border-l text-xs dark:border-l-white/15">
26+
<template x-for="heading in headings" :key="heading.id">
27+
<a
28+
:href="'#' + heading.id"
29+
:class="heading.level === 2 ? 'pb-1 pl-3' : 'py-1 pl-6'"
30+
class="text-gray-600 transition duration-300 ease-in-out will-change-transform hover:translate-x-0.5 hover:text-indigo-500 hover:opacity-100 dark:text-white/80 dark:hover:text-indigo-400"
31+
x-text="heading.text"
32+
></a>
33+
</template>
34+
</nav>
35+
</div>

resources/views/plugin-license.blade.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<x-layout :title="$plugin->name . ' - License'">
22
<section
3-
class="mx-auto mt-10 w-full max-w-7xl"
3+
class="mx-auto mt-10 w-full max-w-3xl px-5 md:mt-14"
44
aria-labelledby="license-title"
55
>
66
<header class="relative">

resources/views/plugin-show.blade.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
class="mx-auto mt-10 w-full max-w-7xl"
44
aria-labelledby="plugin-title"
55
>
6+
@if ($isAdminPreview ?? false)
7+
<div class="mb-6 rounded-xl border border-amber-300 bg-amber-50 p-4 text-center dark:border-amber-600 dark:bg-amber-950/50">
8+
<p class="text-sm font-medium text-amber-800 dark:text-amber-200">
9+
Admin Preview &mdash; This plugin is not yet published. Status: {{ $plugin->status->label() }}
10+
</p>
11+
</div>
12+
@endif
13+
614
<header class="relative">
715
{{-- Blurred circle - Decorative --}}
816
<div
@@ -129,6 +137,11 @@ class="prose min-w-0 max-w-none grow text-gray-600 dark:text-gray-400 dark:prose
129137
"
130138
class="w-full shrink-0 lg:sticky lg:top-24 lg:w-80"
131139
>
140+
{{-- Table of Contents --}}
141+
@if ($plugin->readme_html)
142+
<x-plugin-toc />
143+
@endif
144+
132145
{{-- Purchase Box for Paid Plugins --}}
133146
@if ($plugin->isPaid() && $bestPrice && $plugin->is_active)
134147
<div class="mb-4 rounded-2xl border-2 border-indigo-500 bg-gradient-to-br from-indigo-50 to-purple-50 p-6 dark:border-indigo-400 dark:from-indigo-950/50 dark:to-purple-950/50">
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
namespace Tests\Feature;
4+
5+
use App\Features\ShowPlugins;
6+
use App\Models\Plugin;
7+
use App\Models\User;
8+
use Illuminate\Foundation\Testing\RefreshDatabase;
9+
use Laravel\Pennant\Feature;
10+
use Tests\TestCase;
11+
12+
class AdminPluginPreviewTest extends TestCase
13+
{
14+
use RefreshDatabase;
15+
16+
protected function setUp(): void
17+
{
18+
parent::setUp();
19+
20+
Feature::define(ShowPlugins::class, true);
21+
}
22+
23+
public function test_guest_cannot_view_pending_plugin(): void
24+
{
25+
$plugin = Plugin::factory()->pending()->create();
26+
27+
$this->get(route('plugins.show', $plugin->routeParams()))
28+
->assertStatus(404);
29+
}
30+
31+
public function test_regular_user_cannot_view_pending_plugin(): void
32+
{
33+
$user = User::factory()->create();
34+
$plugin = Plugin::factory()->pending()->create();
35+
36+
$this->actingAs($user)
37+
->get(route('plugins.show', $plugin->routeParams()))
38+
->assertStatus(404);
39+
}
40+
41+
public function test_admin_can_view_pending_plugin(): void
42+
{
43+
$admin = User::factory()->create(['email' => 'admin@test.com']);
44+
config(['filament.users' => ['admin@test.com']]);
45+
46+
$plugin = Plugin::factory()->pending()->create();
47+
48+
$this->actingAs($admin)
49+
->get(route('plugins.show', $plugin->routeParams()))
50+
->assertStatus(200);
51+
}
52+
53+
public function test_admin_sees_preview_banner_on_pending_plugin(): void
54+
{
55+
$admin = User::factory()->create(['email' => 'admin@test.com']);
56+
config(['filament.users' => ['admin@test.com']]);
57+
58+
$plugin = Plugin::factory()->pending()->create();
59+
60+
$this->actingAs($admin)
61+
->get(route('plugins.show', $plugin->routeParams()))
62+
->assertSee('Admin Preview')
63+
->assertSee('Pending Review');
64+
}
65+
66+
public function test_approved_plugin_does_not_show_preview_banner(): void
67+
{
68+
$plugin = Plugin::factory()->approved()->create();
69+
70+
$this->get(route('plugins.show', $plugin->routeParams()))
71+
->assertDontSee('Admin Preview');
72+
}
73+
74+
public function test_admin_can_view_approved_plugin_without_preview_banner(): void
75+
{
76+
$admin = User::factory()->create(['email' => 'admin@test.com']);
77+
config(['filament.users' => ['admin@test.com']]);
78+
79+
$plugin = Plugin::factory()->approved()->create();
80+
81+
$this->actingAs($admin)
82+
->get(route('plugins.show', $plugin->routeParams()))
83+
->assertStatus(200)
84+
->assertDontSee('Admin Preview');
85+
}
86+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace Tests\Feature;
4+
5+
use App\Features\ShowPlugins;
6+
use App\Models\Plugin;
7+
use Illuminate\Foundation\Testing\RefreshDatabase;
8+
use Laravel\Pennant\Feature;
9+
use Tests\TestCase;
10+
11+
class PluginTableOfContentsTest extends TestCase
12+
{
13+
use RefreshDatabase;
14+
15+
protected function setUp(): void
16+
{
17+
parent::setUp();
18+
19+
Feature::define(ShowPlugins::class, true);
20+
}
21+
22+
public function test_toc_component_rendered_when_readme_has_content(): void
23+
{
24+
$plugin = Plugin::factory()->approved()->create([
25+
'readme_html' => '<h2 id="installation">Installation</h2><p>Steps here.</p><h2 id="usage">Usage</h2><p>More content.</p>',
26+
]);
27+
28+
$this->get(route('plugins.show', $plugin->routeParams()))
29+
->assertStatus(200)
30+
->assertSee('On this page');
31+
}
32+
33+
public function test_toc_component_not_rendered_when_no_readme(): void
34+
{
35+
$plugin = Plugin::factory()->approved()->create([
36+
'readme_html' => null,
37+
]);
38+
39+
$this->get(route('plugins.show', $plugin->routeParams()))
40+
->assertStatus(200)
41+
->assertDontSee('On this page');
42+
}
43+
}

0 commit comments

Comments
 (0)