diff --git a/app/Filament/Resources/PluginResource.php b/app/Filament/Resources/PluginResource.php index 85ae8f48..2d8f2eef 100644 --- a/app/Filament/Resources/PluginResource.php +++ b/app/Filament/Resources/PluginResource.php @@ -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), @@ -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(''.e($record->repository_url).' ↗') + : '-'), + + 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(''.e($license).' ↗'); + } + + return $license; + }) + ->visible(fn (?Plugin $record) => $record !== null), Forms\Components\Select::make('status') ->options(PluginStatus::class) @@ -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') diff --git a/app/Filament/Resources/PluginResource/Pages/EditPlugin.php b/app/Filament/Resources/PluginResource/Pages/EditPlugin.php index f3c63ce7..01225f01 100644 --- a/app/Filament/Resources/PluginResource/Pages/EditPlugin.php +++ b/app/Filament/Resources/PluginResource/Pages/EditPlugin.php @@ -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') @@ -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') @@ -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') @@ -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'), ]; diff --git a/package-lock.json b/package-lock.json index c145fd92..88744b28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "agile-sloth", + "name": "tender-duck", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/resources/views/components/navbar/mobile-menu.blade.php b/resources/views/components/navbar/mobile-menu.blade.php index e2a6d95d..5afaf4b2 100644 --- a/resources/views/components/navbar/mobile-menu.blade.php +++ b/resources/views/components/navbar/mobile-menu.blade.php @@ -338,7 +338,7 @@ class="size-4 shrink-0" /> @endif -
Dashboard
+
Log in
@endauth diff --git a/resources/views/livewire/customer/plugins/show.blade.php b/resources/views/livewire/customer/plugins/show.blade.php index b0b234c9..636a3275 100644 --- a/resources/views/livewire/customer/plugins/show.blade.php +++ b/resources/views/livewire/customer/plugins/show.blade.php @@ -132,7 +132,7 @@ @if ($check['key'] === 'webhook_configured') @if (! $isPassing) -
+

We couldn't automatically install the webhook. Please set it up manually:

@@ -166,9 +166,9 @@
@elseif ($plugin->webhook_secret) -
-
diff --git a/resources/views/plugin-show.blade.php b/resources/views/plugin-show.blade.php index c4ca47f4..b87c7d36 100644 --- a/resources/views/plugin-show.blade.php +++ b/resources/views/plugin-show.blade.php @@ -354,10 +354,11 @@ class="inline-flex items-center gap-1 text-sm font-medium text-indigo-600 hover: - {{ $plugin->support_channel }} - + {{ Str::limit(preg_replace('#^https?://#', '', $plugin->support_channel), 25) }} + @elseif (filter_var($plugin->support_channel, FILTER_VALIDATE_EMAIL)) 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'); + } +} diff --git a/tests/Feature/PluginShowSupportChannelTest.php b/tests/Feature/PluginShowSupportChannelTest.php new file mode 100644 index 00000000..64cd44c8 --- /dev/null +++ b/tests/Feature/PluginShowSupportChannelTest.php @@ -0,0 +1,57 @@ +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); + } +}