Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/Filament/Resources/PluginResource/Pages/EditPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion app/Filament/Resources/PluginResource/Pages/ViewPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
11 changes: 7 additions & 4 deletions app/Http/Controllers/PluginDirectoryController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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(),
]);
}

Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions resources/views/components/plugin-toc.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<div
x-data="{
headings: [],
init() {
const article = document.querySelector('article')
if (! article) return

const elements = article.querySelectorAll('h2[id], h3[id]')
this.headings = Array.from(elements).map(el => ({
id: el.id,
text: el.textContent.replace(/^#\s*/, '').trim(),
level: parseInt(el.tagName.substring(1)),
}))
},
}"
x-show="headings.length > 0"
x-cloak
class="mb-6"
>
<h3 class="flex items-center gap-1.5 text-sm opacity-60">
<x-icons.stacked-lines class="size-[18px]" />
<div>On this page</div>
</h3>

<div class="mt-4 flex flex-col space-y-2 overflow-y-auto overflow-x-hidden border-l text-xs dark:border-l-white/15">
<template x-for="heading in headings" :key="heading.id">
<a
:href="'#' + heading.id"
:class="heading.level === 2 ? 'pb-1 pl-3' : 'py-1 pl-6'"
class="transition duration-300 ease-in-out will-change-transform hover:translate-x-0.5 hover:text-violet-400 hover:opacity-100 dark:text-white/80"
x-text="heading.text"
></a>
</template>
</div>
</div>
2 changes: 1 addition & 1 deletion resources/views/plugin-license.blade.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<x-layout :title="$plugin->name . ' - License'">
<section
class="mx-auto mt-10 w-full max-w-7xl"
class="mx-auto mt-10 w-full max-w-3xl px-5 md:mt-14"
aria-labelledby="license-title"
>
<header class="relative">
Expand Down
51 changes: 33 additions & 18 deletions resources/views/plugin-show.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
class="mx-auto mt-10 w-full max-w-7xl"
aria-labelledby="plugin-title"
>
@if ($isAdminPreview ?? false)
<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">
<p class="text-sm font-medium text-amber-800 dark:text-amber-200">
Admin Preview &mdash; This plugin is not yet published. Status: {{ $plugin->status->label() }}
</p>
</div>
@endif

<header class="relative">
{{-- Blurred circle - Decorative --}}
<div
Expand Down Expand Up @@ -88,31 +96,37 @@ class="font-mono text-2xl font-bold sm:text-3xl"

<div class="mt-2 flex flex-col-reverse gap-8 lg:flex-row lg:items-start">
{{-- Main content - README --}}
<article
x-init="
() => {
motion.inView($el, () => {
gsap.fromTo(
$el,
{ autoAlpha: 0, y: 5 },
{ autoAlpha: 1, y: 0, duration: 0.7, ease: 'power1.out' },
)
})
}
"
class="prose min-w-0 max-w-none grow text-gray-600 dark:text-gray-400 dark:prose-headings:text-white"
aria-labelledby="plugin-title"
>
<div class="min-w-0 grow">
@if ($plugin->readme_html)
{!! $plugin->readme_html !!}
<x-plugin-toc />
@endif

<article
x-init="
() => {
motion.inView($el, () => {
gsap.fromTo(
$el,
{ autoAlpha: 0, y: 5 },
{ autoAlpha: 1, y: 0, duration: 0.7, ease: 'power1.out' },
)
})
}
"
class="prose min-w-0 max-w-none grow text-gray-600 dark:text-gray-400 dark:prose-headings:text-white"
aria-labelledby="plugin-title"
>
@if ($plugin->readme_html)
{!! $plugin->readme_html !!}
@else
<div class="rounded-xl border border-gray-200 bg-gray-50 p-8 text-center dark:border-gray-700 dark:bg-slate-800/50">
<p class="text-gray-500 dark:text-gray-400">
README not available yet.
</p>
</div>
@endif
</article>
@endif
</article>
</div>

{{-- Sidebar - Plugin details --}}
<aside
Expand Down Expand Up @@ -329,6 +343,7 @@ class="size-10 shrink-0 rounded-lg object-cover"
</ul>
</div>
@endif

</aside>
</div>
</section>
Expand Down
86 changes: 86 additions & 0 deletions tests/Feature/AdminPluginPreviewTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

namespace Tests\Feature;

use App\Features\ShowPlugins;
use App\Models\Plugin;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Pennant\Feature;
use Tests\TestCase;

class AdminPluginPreviewTest extends TestCase
{
use RefreshDatabase;

protected function setUp(): void
{
parent::setUp();

Feature::define(ShowPlugins::class, true);
}

public function test_guest_cannot_view_pending_plugin(): void
{
$plugin = Plugin::factory()->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');
}
}
43 changes: 43 additions & 0 deletions tests/Feature/PluginTableOfContentsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace Tests\Feature;

use App\Features\ShowPlugins;
use App\Models\Plugin;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Pennant\Feature;
use Tests\TestCase;

class PluginTableOfContentsTest extends TestCase
{
use RefreshDatabase;

protected function setUp(): void
{
parent::setUp();

Feature::define(ShowPlugins::class, true);
}

public function test_toc_component_rendered_when_readme_has_content(): void
{
$plugin = Plugin::factory()->approved()->create([
'readme_html' => '<h2 id="installation">Installation</h2><p>Steps here.</p><h2 id="usage">Usage</h2><p>More content.</p>',
]);

$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');
}
}
Loading