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
38 changes: 30 additions & 8 deletions app/Filament/Resources/PluginResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,12 @@ public static function form(Schema $schema): Schema
: 'No logo')
->visible(fn (?Plugin $record) => $record !== null),

Forms\Components\TextInput::make('name')
->label('Composer Package Name'),
Forms\Components\TextInput::make('display_name')
->label('Display Name'),

Forms\Components\Placeholder::make('name')
->label('Composer Package Name')
->content(fn (?Plugin $record) => $record?->name ?? '-'),

Forms\Components\Select::make('type')
->options(PluginType::class),
Expand All @@ -67,12 +71,29 @@ public static function form(Schema $schema): Schema
->placeholder('No tier')
->helperText('Set pricing tier for paid plugins'),

Forms\Components\TextInput::make('repository_url')
Forms\Components\Placeholder::make('repository_url')
->label('Repository URL')
->content(fn (?Plugin $record) => $record?->repository_url
? new HtmlString('<a href="'.e($record->repository_url).'" target="_blank" rel="noopener noreferrer" class="text-primary-600 hover:underline">'.e($record->repository_url).' ↗</a>')
: '-'),

Forms\Components\Placeholder::make('license_type')
->label('License')
->content(function (?Plugin $record) {
$license = $record?->getLicense();
$licenseUrl = $record?->getLicenseUrl();

if (! $license) {
return '-';
}

->url()
->suffixIcon('heroicon-o-arrow-top-right-on-square')
->suffixIconColor('gray'),
if ($licenseUrl) {
return new HtmlString('<a href="'.e($licenseUrl).'" target="_blank" rel="noopener noreferrer" class="text-primary-600 hover:underline">'.e($license).' ↗</a>');
}

return $license;
})
->visible(fn (?Plugin $record) => $record !== null),

Forms\Components\Select::make('status')
->options(PluginStatus::class)
Expand Down Expand Up @@ -193,8 +214,9 @@ public static function form(Schema $schema): Schema
->searchable()
->preload(),

Forms\Components\DateTimePicker::make('created_at')
->label('Submitted At'),
Forms\Components\Placeholder::make('created_at')
->label('Submitted At')
->content(fn (?Plugin $record) => $record?->created_at?->format('M j, Y g:i A') ?? '-'),

Forms\Components\Select::make('approved_by')
->relationship('approvedBy', 'email')
Expand Down
69 changes: 35 additions & 34 deletions app/Filament/Resources/PluginResource/Pages/EditPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,34 +24,34 @@ class EditPlugin extends EditRecord
protected function getHeaderActions(): array
{
return [
Actions\ActionGroup::make([
Actions\Action::make('approve')
->icon('heroicon-o-check')
->color('success')
->visible(fn () => $this->record->isPending())
->disabled(fn () => ! $this->record->passesRequiredReviewChecks())
->action(fn () => $this->record->approve(auth()->id()))
->requiresConfirmation()
->modalHeading('Approve Plugin')
->modalDescription(fn () => ! $this->record->passesRequiredReviewChecks()
? "Cannot approve '{$this->record->name}' — required checks are failing: ".implode(', ', $this->record->getFailingRequiredChecks())
: "Are you sure you want to approve '{$this->record->name}'?"),
Actions\Action::make('approve')
->icon('heroicon-o-check')
->color('success')
->visible(fn () => $this->record->isPending())
->disabled(fn () => ! $this->record->passesRequiredReviewChecks())
->action(fn () => $this->record->approve(auth()->id()))
->requiresConfirmation()
->modalHeading('Approve Plugin')
->modalDescription(fn () => ! $this->record->passesRequiredReviewChecks()
? "Cannot approve '{$this->record->name}' — required checks are failing: ".implode(', ', $this->record->getFailingRequiredChecks())
: "Are you sure you want to approve '{$this->record->name}'?"),

Actions\Action::make('reject')
->icon('heroicon-o-x-mark')
->color('danger')
->visible(fn () => $this->record->isPending() || $this->record->isApproved())
->form([
Forms\Components\Textarea::make('rejection_reason')
->label('Reason for Rejection')
->required()
->rows(3)
->placeholder('Please explain why this plugin is being rejected...'),
])
->action(fn (array $data) => $this->record->reject($data['rejection_reason'], auth()->id()))
->modalHeading('Reject Plugin')
->modalDescription(fn () => "Are you sure you want to reject '{$this->record->name}'?"),
Actions\Action::make('reject')
->icon('heroicon-o-x-mark')
->color('danger')
->visible(fn () => $this->record->isPending() || $this->record->isApproved())
->form([
Forms\Components\Textarea::make('rejection_reason')
->label('Reason for Rejection')
->required()
->rows(3)
->placeholder('Please explain why this plugin is being rejected...'),
])
->action(fn (array $data) => $this->record->reject($data['rejection_reason'], auth()->id()))
->modalHeading('Reject Plugin')
->modalDescription(fn () => "Are you sure you want to reject '{$this->record->name}'?"),

Actions\ActionGroup::make([
Actions\Action::make('convertToPaid')
->label('Convert to Paid')
->icon('heroicon-o-currency-dollar')
Expand Down Expand Up @@ -106,6 +106,7 @@ protected function getHeaderActions(): array
->label('Grant to User')
->icon('heroicon-o-gift')
->color('success')
->visible(fn () => $this->record->isApproved())
->form([
Forms\Components\Select::make('user_id')
->label('User')
Expand Down Expand Up @@ -158,14 +159,6 @@ protected function getHeaderActions(): array
->modalDescription(fn () => "Grant '{$this->record->name}' to a user for free.")
->modalSubmitActionLabel('Grant'),

Actions\Action::make('viewListing')
->label('View Listing Page')
->icon('heroicon-o-eye')
->color('gray')
->url(fn () => route('plugins.show', $this->record->routeParams()))
->openUrlInNewTab()
->visible(fn () => $this->record->isApproved() || $this->record->isPending()),

Actions\Action::make('viewPackagist')
->label('View on Packagist')
->icon('heroicon-o-arrow-top-right-on-square')
Expand Down Expand Up @@ -259,6 +252,14 @@ protected function getHeaderActions(): array
->visible(fn () => $this->record->repository_url !== null)
->url(fn () => $this->record->getGithubUrl())
->openUrlInNewTab(),

Actions\Action::make('viewListing')
->label('View Listing Page')
->icon('heroicon-o-eye')
->color('gray')
->url(fn () => route('plugins.show', $this->record->routeParams()))
->openUrlInNewTab()
->visible(fn () => $this->record->isApproved() || $this->record->isPending()),
])
->icon('heroicon-m-ellipsis-vertical'),
];
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.

2 changes: 1 addition & 1 deletion resources/views/components/navbar/mobile-menu.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ class="size-4 shrink-0"
/>
@endif

<div>Dashboard</div>
<div>Log in</div>
</a>
@endauth
</div>
Expand Down
8 changes: 4 additions & 4 deletions resources/views/livewire/customer/plugins/show.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@

@if ($check['key'] === 'webhook_configured')
@if (! $isPassing)
<div class="ml-7 mt-2 rounded-md border border-amber-200 bg-amber-50 p-4 dark:border-amber-900/50 dark:bg-amber-900/20">
<div wire:key="webhook-status-failing" class="ml-7 mt-2 rounded-md border border-amber-200 bg-amber-50 p-4 dark:border-amber-900/50 dark:bg-amber-900/20">
<p class="text-sm text-amber-800 dark:text-amber-200">
We couldn't automatically install the webhook. Please set it up manually:
</p>
Expand Down Expand Up @@ -166,9 +166,9 @@
</div>
</div>
@elseif ($plugin->webhook_secret)
<div class="ml-7 mt-1" x-data="{ open: false }">
<button type="button" @click="open = !open" class="inline-flex items-center gap-1 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300">
<x-heroicon-o-chevron-right class="size-3 transition-transform" ::class="open && 'rotate-90'" />
<div wire:key="webhook-status-passing" class="ml-7 mt-1" x-data="{ open: false }">
<button type="button" x-on:click="open = !open" class="inline-flex items-center gap-1 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300">
<x-heroicon-o-chevron-right class="size-3 transition-transform" x-bind:class="open && 'rotate-90'" />
View setup instructions
</button>
<div x-show="open" x-cloak class="mt-2 rounded-md border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50">
Expand Down
5 changes: 3 additions & 2 deletions resources/views/plugin-show.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -354,10 +354,11 @@ class="inline-flex items-center gap-1 text-sm font-medium text-indigo-600 hover:
<a
href="{{ $plugin->support_channel }}"
target="_blank"
title="{{ $plugin->support_channel }}"
class="inline-flex items-center gap-1 text-sm font-medium text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300"
>
{{ $plugin->support_channel }}
<x-heroicon-o-arrow-top-right-on-square class="size-3" />
{{ Str::limit(preg_replace('#^https?://#', '', $plugin->support_channel), 25) }}
<x-heroicon-o-arrow-top-right-on-square class="size-3 shrink-0" />
</a>
@elseif (filter_var($plugin->support_channel, FILTER_VALIDATE_EMAIL))
<a
Expand Down
108 changes: 108 additions & 0 deletions tests/Feature/Filament/PluginEditFormTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

namespace Tests\Feature\Filament;

use App\Filament\Resources\PluginResource\Pages\EditPlugin;
use App\Models\Plugin;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\TestCase;

class PluginEditFormTest extends TestCase
{
use RefreshDatabase;

private User $admin;

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

$this->admin = User::factory()->create(['email' => 'admin@test.com']);
config(['filament.users' => ['admin@test.com']]);
}

public function test_composer_package_name_is_not_editable(): void
{
$plugin = Plugin::factory()->approved()->create();

Livewire::actingAs($this->admin)
->test(EditPlugin::class, ['record' => $plugin->getRouteKey()])
->assertFormFieldDoesNotExist('name');
}

public function test_repository_url_is_not_editable(): void
{
$plugin = Plugin::factory()->approved()->create();

Livewire::actingAs($this->admin)
->test(EditPlugin::class, ['record' => $plugin->getRouteKey()])
->assertFormFieldDoesNotExist('repository_url');
}

public function test_submitted_at_is_not_editable(): void
{
$plugin = Plugin::factory()->approved()->create();

Livewire::actingAs($this->admin)
->test(EditPlugin::class, ['record' => $plugin->getRouteKey()])
->assertFormFieldDoesNotExist('created_at');
}

public function test_display_name_is_editable(): void
{
$plugin = Plugin::factory()->approved()->create(['display_name' => 'My Cool Plugin']);

Livewire::actingAs($this->admin)
->test(EditPlugin::class, ['record' => $plugin->getRouteKey()])
->assertFormFieldExists('display_name');
}

public function test_edit_page_renders_license_type(): void
{
$plugin = Plugin::factory()->approved()->create([
'composer_data' => ['license' => 'MIT'],
]);

Livewire::actingAs($this->admin)
->test(EditPlugin::class, ['record' => $plugin->getRouteKey()])
->assertSee('MIT');
}

public function test_edit_page_renders_package_name_as_text(): void
{
$plugin = Plugin::factory()->approved()->create(['name' => 'vendor/my-plugin']);

Livewire::actingAs($this->admin)
->test(EditPlugin::class, ['record' => $plugin->getRouteKey()])
->assertSee('vendor/my-plugin');
}

public function test_approve_action_is_visible_for_pending_plugin(): void
{
$plugin = Plugin::factory()->pending()->create();

Livewire::actingAs($this->admin)
->test(EditPlugin::class, ['record' => $plugin->getRouteKey()])
->assertActionVisible('approve');
}

public function test_reject_action_is_visible_for_pending_plugin(): void
{
$plugin = Plugin::factory()->pending()->create();

Livewire::actingAs($this->admin)
->test(EditPlugin::class, ['record' => $plugin->getRouteKey()])
->assertActionVisible('reject');
}

public function test_approve_action_is_hidden_for_approved_plugin(): void
{
$plugin = Plugin::factory()->approved()->create();

Livewire::actingAs($this->admin)
->test(EditPlugin::class, ['record' => $plugin->getRouteKey()])
->assertActionHidden('approve');
}
}
57 changes: 57 additions & 0 deletions tests/Feature/PluginShowSupportChannelTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?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 PluginShowSupportChannelTest extends TestCase
{
use RefreshDatabase;

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

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

public function test_long_support_url_is_truncated_in_display_text(): void
{
$plugin = Plugin::factory()->approved()->create([
'support_channel' => 'https://github.com/SRWieZ/nativephp-mobile-packages',
]);

$response = $this->get(route('plugins.show', $plugin->routeParams()));

$response->assertStatus(200);
$response->assertSee('github.com/SRWieZ/nativep...', false);
}

public function test_support_url_display_strips_protocol(): void
{
$plugin = Plugin::factory()->approved()->create([
'support_channel' => 'https://example.com/support',
]);

$response = $this->get(route('plugins.show', $plugin->routeParams()));

$response->assertStatus(200);
$response->assertSee('example.com/support', false);
}

public function test_email_support_channel_is_not_truncated(): void
{
$plugin = Plugin::factory()->approved()->create([
'support_channel' => 'support@example.com',
]);

$response = $this->get(route('plugins.show', $plugin->routeParams()));

$response->assertStatus(200);
$response->assertSee('support@example.com', false);
}
}
Loading