Skip to content

Commit 16d5476

Browse files
simonhampclaude
andcommitted
Improve admin plugin edit: read-only fields, license display, action visibility
- Make composer package name, repository URL, and submitted_at read-only - Add editable display name field and license type with GitHub link - Move approve/reject actions to top-level header for pending plugins - Restrict Grant to User action to approved plugins only - Move View Listing Page to bottom of action menu - Fix webhook instructions toggle after successful retry (wire:key) - Fix chevron rotation on View setup instructions toggle (x-bind:class) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b8ba696 commit 16d5476

4 files changed

Lines changed: 177 additions & 46 deletions

File tree

app/Filament/Resources/PluginResource.php

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,12 @@ public static function form(Schema $schema): Schema
5656
: 'No logo')
5757
->visible(fn (?Plugin $record) => $record !== null),
5858

59-
Forms\Components\TextInput::make('name')
60-
->label('Composer Package Name'),
59+
Forms\Components\TextInput::make('display_name')
60+
->label('Display Name'),
61+
62+
Forms\Components\Placeholder::make('name')
63+
->label('Composer Package Name')
64+
->content(fn (?Plugin $record) => $record?->name ?? '-'),
6165

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

70-
Forms\Components\TextInput::make('repository_url')
74+
Forms\Components\Placeholder::make('repository_url')
7175
->label('Repository URL')
76+
->content(fn (?Plugin $record) => $record?->repository_url
77+
? new HtmlString('<a href="'.e($record->repository_url).'" target="_blank" rel="noopener noreferrer" class="text-primary-600 hover:underline">'.e($record->repository_url).' ↗</a>')
78+
: '-'),
79+
80+
Forms\Components\Placeholder::make('license_type')
81+
->label('License')
82+
->content(function (?Plugin $record) {
83+
$license = $record?->getLicense();
84+
$licenseUrl = $record?->getLicenseUrl();
85+
86+
if (! $license) {
87+
return '-';
88+
}
7289

73-
->url()
74-
->suffixIcon('heroicon-o-arrow-top-right-on-square')
75-
->suffixIconColor('gray'),
90+
if ($licenseUrl) {
91+
return new HtmlString('<a href="'.e($licenseUrl).'" target="_blank" rel="noopener noreferrer" class="text-primary-600 hover:underline">'.e($license).' ↗</a>');
92+
}
93+
94+
return $license;
95+
})
96+
->visible(fn (?Plugin $record) => $record !== null),
7697

7798
Forms\Components\Select::make('status')
7899
->options(PluginStatus::class)
@@ -193,8 +214,9 @@ public static function form(Schema $schema): Schema
193214
->searchable()
194215
->preload(),
195216

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

199221
Forms\Components\Select::make('approved_by')
200222
->relationship('approvedBy', 'email')

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

Lines changed: 35 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -24,34 +24,34 @@ class EditPlugin extends EditRecord
2424
protected function getHeaderActions(): array
2525
{
2626
return [
27-
Actions\ActionGroup::make([
28-
Actions\Action::make('approve')
29-
->icon('heroicon-o-check')
30-
->color('success')
31-
->visible(fn () => $this->record->isPending())
32-
->disabled(fn () => ! $this->record->passesRequiredReviewChecks())
33-
->action(fn () => $this->record->approve(auth()->id()))
34-
->requiresConfirmation()
35-
->modalHeading('Approve Plugin')
36-
->modalDescription(fn () => ! $this->record->passesRequiredReviewChecks()
37-
? "Cannot approve '{$this->record->name}' — required checks are failing: ".implode(', ', $this->record->getFailingRequiredChecks())
38-
: "Are you sure you want to approve '{$this->record->name}'?"),
27+
Actions\Action::make('approve')
28+
->icon('heroicon-o-check')
29+
->color('success')
30+
->visible(fn () => $this->record->isPending())
31+
->disabled(fn () => ! $this->record->passesRequiredReviewChecks())
32+
->action(fn () => $this->record->approve(auth()->id()))
33+
->requiresConfirmation()
34+
->modalHeading('Approve Plugin')
35+
->modalDescription(fn () => ! $this->record->passesRequiredReviewChecks()
36+
? "Cannot approve '{$this->record->name}' — required checks are failing: ".implode(', ', $this->record->getFailingRequiredChecks())
37+
: "Are you sure you want to approve '{$this->record->name}'?"),
3938

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

54+
Actions\ActionGroup::make([
5555
Actions\Action::make('convertToPaid')
5656
->label('Convert to Paid')
5757
->icon('heroicon-o-currency-dollar')
@@ -106,6 +106,7 @@ protected function getHeaderActions(): array
106106
->label('Grant to User')
107107
->icon('heroicon-o-gift')
108108
->color('success')
109+
->visible(fn () => $this->record->isApproved())
109110
->form([
110111
Forms\Components\Select::make('user_id')
111112
->label('User')
@@ -158,14 +159,6 @@ protected function getHeaderActions(): array
158159
->modalDescription(fn () => "Grant '{$this->record->name}' to a user for free.")
159160
->modalSubmitActionLabel('Grant'),
160161

161-
Actions\Action::make('viewListing')
162-
->label('View Listing Page')
163-
->icon('heroicon-o-eye')
164-
->color('gray')
165-
->url(fn () => route('plugins.show', $this->record->routeParams()))
166-
->openUrlInNewTab()
167-
->visible(fn () => $this->record->isApproved() || $this->record->isPending()),
168-
169162
Actions\Action::make('viewPackagist')
170163
->label('View on Packagist')
171164
->icon('heroicon-o-arrow-top-right-on-square')
@@ -259,6 +252,14 @@ protected function getHeaderActions(): array
259252
->visible(fn () => $this->record->repository_url !== null)
260253
->url(fn () => $this->record->getGithubUrl())
261254
->openUrlInNewTab(),
255+
256+
Actions\Action::make('viewListing')
257+
->label('View Listing Page')
258+
->icon('heroicon-o-eye')
259+
->color('gray')
260+
->url(fn () => route('plugins.show', $this->record->routeParams()))
261+
->openUrlInNewTab()
262+
->visible(fn () => $this->record->isApproved() || $this->record->isPending()),
262263
])
263264
->icon('heroicon-m-ellipsis-vertical'),
264265
];

resources/views/livewire/customer/plugins/show.blade.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@
132132

133133
@if ($check['key'] === 'webhook_configured')
134134
@if (! $isPassing)
135-
<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">
135+
<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">
136136
<p class="text-sm text-amber-800 dark:text-amber-200">
137137
We couldn't automatically install the webhook. Please set it up manually:
138138
</p>
@@ -166,9 +166,9 @@
166166
</div>
167167
</div>
168168
@elseif ($plugin->webhook_secret)
169-
<div class="ml-7 mt-1" x-data="{ open: false }">
170-
<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">
171-
<x-heroicon-o-chevron-right class="size-3 transition-transform" ::class="open && 'rotate-90'" />
169+
<div wire:key="webhook-status-passing" class="ml-7 mt-1" x-data="{ open: false }">
170+
<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">
171+
<x-heroicon-o-chevron-right class="size-3 transition-transform" x-bind:class="open && 'rotate-90'" />
172172
View setup instructions
173173
</button>
174174
<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">
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
namespace Tests\Feature\Filament;
4+
5+
use App\Filament\Resources\PluginResource\Pages\EditPlugin;
6+
use App\Models\Plugin;
7+
use App\Models\User;
8+
use Illuminate\Foundation\Testing\RefreshDatabase;
9+
use Livewire\Livewire;
10+
use Tests\TestCase;
11+
12+
class PluginEditFormTest extends TestCase
13+
{
14+
use RefreshDatabase;
15+
16+
private User $admin;
17+
18+
protected function setUp(): void
19+
{
20+
parent::setUp();
21+
22+
$this->admin = User::factory()->create(['email' => 'admin@test.com']);
23+
config(['filament.users' => ['admin@test.com']]);
24+
}
25+
26+
public function test_composer_package_name_is_not_editable(): void
27+
{
28+
$plugin = Plugin::factory()->approved()->create();
29+
30+
Livewire::actingAs($this->admin)
31+
->test(EditPlugin::class, ['record' => $plugin->getRouteKey()])
32+
->assertFormFieldDoesNotExist('name');
33+
}
34+
35+
public function test_repository_url_is_not_editable(): void
36+
{
37+
$plugin = Plugin::factory()->approved()->create();
38+
39+
Livewire::actingAs($this->admin)
40+
->test(EditPlugin::class, ['record' => $plugin->getRouteKey()])
41+
->assertFormFieldDoesNotExist('repository_url');
42+
}
43+
44+
public function test_submitted_at_is_not_editable(): void
45+
{
46+
$plugin = Plugin::factory()->approved()->create();
47+
48+
Livewire::actingAs($this->admin)
49+
->test(EditPlugin::class, ['record' => $plugin->getRouteKey()])
50+
->assertFormFieldDoesNotExist('created_at');
51+
}
52+
53+
public function test_display_name_is_editable(): void
54+
{
55+
$plugin = Plugin::factory()->approved()->create(['display_name' => 'My Cool Plugin']);
56+
57+
Livewire::actingAs($this->admin)
58+
->test(EditPlugin::class, ['record' => $plugin->getRouteKey()])
59+
->assertFormFieldExists('display_name');
60+
}
61+
62+
public function test_edit_page_renders_license_type(): void
63+
{
64+
$plugin = Plugin::factory()->approved()->create([
65+
'composer_data' => ['license' => 'MIT'],
66+
]);
67+
68+
Livewire::actingAs($this->admin)
69+
->test(EditPlugin::class, ['record' => $plugin->getRouteKey()])
70+
->assertSee('MIT');
71+
}
72+
73+
public function test_edit_page_renders_package_name_as_text(): void
74+
{
75+
$plugin = Plugin::factory()->approved()->create(['name' => 'vendor/my-plugin']);
76+
77+
Livewire::actingAs($this->admin)
78+
->test(EditPlugin::class, ['record' => $plugin->getRouteKey()])
79+
->assertSee('vendor/my-plugin');
80+
}
81+
82+
public function test_approve_action_is_visible_for_pending_plugin(): void
83+
{
84+
$plugin = Plugin::factory()->pending()->create();
85+
86+
Livewire::actingAs($this->admin)
87+
->test(EditPlugin::class, ['record' => $plugin->getRouteKey()])
88+
->assertActionVisible('approve');
89+
}
90+
91+
public function test_reject_action_is_visible_for_pending_plugin(): void
92+
{
93+
$plugin = Plugin::factory()->pending()->create();
94+
95+
Livewire::actingAs($this->admin)
96+
->test(EditPlugin::class, ['record' => $plugin->getRouteKey()])
97+
->assertActionVisible('reject');
98+
}
99+
100+
public function test_approve_action_is_hidden_for_approved_plugin(): void
101+
{
102+
$plugin = Plugin::factory()->approved()->create();
103+
104+
Livewire::actingAs($this->admin)
105+
->test(EditPlugin::class, ['record' => $plugin->getRouteKey()])
106+
->assertActionHidden('approve');
107+
}
108+
}

0 commit comments

Comments
 (0)