From f4ba9ad091dbfaf166f8c0889a86c296620ded2f Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Fri, 27 Mar 2026 17:35:24 +0000 Subject: [PATCH] Add notes and support channel fields to plugin submissions Developers can now provide optional notes and a support channel (email or URL) when submitting a plugin. Both fields are shown only after selecting a repository. They are displayed read-only in the Filament admin dashboard and are not shown on public listings. Co-Authored-By: Claude Opus 4.6 --- app/Filament/Resources/PluginResource.php | 12 + app/Livewire/Customer/Plugins/Create.php | 8 + database/factories/PluginFactory.php | 14 ++ ...3_27_160758_add_notes_to_plugins_table.php | 29 +++ .../customer/plugins/create.blade.php | 28 ++- tests/Feature/PluginSubmissionNotesTest.php | 212 ++++++++++++++++++ 6 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 database/migrations/2026_03_27_160758_add_notes_to_plugins_table.php create mode 100644 tests/Feature/PluginSubmissionNotesTest.php diff --git a/app/Filament/Resources/PluginResource.php b/app/Filament/Resources/PluginResource.php index 1ea7a3750..9391ceca2 100644 --- a/app/Filament/Resources/PluginResource.php +++ b/app/Filament/Resources/PluginResource.php @@ -154,6 +154,18 @@ public static function form(Schema $schema): Schema ]) ->visible(fn (?Plugin $record) => $record?->review_checks !== null), + Schemas\Components\Section::make('Developer Submission Details') + ->schema([ + Forms\Components\Placeholder::make('support_channel_display') + ->label('Support Channel') + ->content(fn (?Plugin $record) => $record?->support_channel ?? 'Not provided'), + + Forms\Components\Placeholder::make('notes_display') + ->label('Notes') + ->content(fn (?Plugin $record) => $record?->notes ?? 'Not provided'), + ]) + ->visible(fn (?Plugin $record) => $record !== null), + Schemas\Components\Section::make('Submission Info') ->schema([ Forms\Components\Select::make('user_id') diff --git a/app/Livewire/Customer/Plugins/Create.php b/app/Livewire/Customer/Plugins/Create.php index f46124453..aff451191 100644 --- a/app/Livewire/Customer/Plugins/Create.php +++ b/app/Livewire/Customer/Plugins/Create.php @@ -22,6 +22,10 @@ class Create extends Component public string $repository = ''; + public string $notes = ''; + + public string $supportChannel = ''; + /** @var array */ public array $repositories = []; @@ -90,6 +94,8 @@ function ($attribute, $value, $fail): void { }, ], 'pluginType' => ['required', 'string', 'in:free,paid'], + 'notes' => ['nullable', 'string', 'max:5000'], + 'supportChannel' => ['nullable', 'string', 'max:255'], ], [ 'repository.required' => 'Please select a repository for your plugin.', 'repository.regex' => 'Please enter a valid repository in the format vendor/repo-name.', @@ -115,6 +121,8 @@ function ($attribute, $value, $fail): void { 'type' => $this->pluginType, 'status' => PluginStatus::Pending, 'developer_account_id' => $developerAccountId, + 'notes' => $this->notes ?: null, + 'support_channel' => $this->supportChannel ?: null, ]); $webhookSecret = $plugin->generateWebhookSecret(); diff --git a/database/factories/PluginFactory.php b/database/factories/PluginFactory.php index 9d8e21d5b..8b0a9734a 100644 --- a/database/factories/PluginFactory.php +++ b/database/factories/PluginFactory.php @@ -185,4 +185,18 @@ public function withoutDescription(): static 'description' => null, ]); } + + public function withNotes(?string $notes = null): static + { + return $this->state(fn (array $attributes) => [ + 'notes' => $notes ?? fake()->sentence(), + ]); + } + + public function withSupportChannel(?string $channel = null): static + { + return $this->state(fn (array $attributes) => [ + 'support_channel' => $channel ?? fake()->safeEmail(), + ]); + } } diff --git a/database/migrations/2026_03_27_160758_add_notes_to_plugins_table.php b/database/migrations/2026_03_27_160758_add_notes_to_plugins_table.php new file mode 100644 index 000000000..2ab4b7094 --- /dev/null +++ b/database/migrations/2026_03_27_160758_add_notes_to_plugins_table.php @@ -0,0 +1,29 @@ +text('notes')->nullable()->after('description'); + $table->string('support_channel')->nullable()->after('notes'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('plugins', function (Blueprint $table) { + $table->dropColumn(['notes', 'support_channel']); + }); + } +}; diff --git a/resources/views/livewire/customer/plugins/create.blade.php b/resources/views/livewire/customer/plugins/create.blade.php index cd93a94bd..6968c2ace 100644 --- a/resources/views/livewire/customer/plugins/create.blade.php +++ b/resources/views/livewire/customer/plugins/create.blade.php @@ -112,7 +112,7 @@ Loading repositories... @elseif($reposLoaded) - + @foreach($repositories as $repo) {{ $repo['full_name'] }}{{ $repo['private'] ? ' (private)' : '' }} @@ -153,6 +153,32 @@ @endif @endfeature + @if($repository) + {{-- Support Channel --}} + + Support Channel + + How can users get support for your plugin? Provide an email address or a URL. If you enter a URL, ensure that it clearly details how a visitor goes about getting support for this plugin. + + +
+ +
+
+ + {{-- Notes --}} + + Notes + + Any notes for the review team? Feel free to share links to videos of the plugin working. These won't be displayed on your plugin listing. + + +
+ +
+
+ @endif + {{-- Submit Button --}}
Cancel diff --git a/tests/Feature/PluginSubmissionNotesTest.php b/tests/Feature/PluginSubmissionNotesTest.php new file mode 100644 index 000000000..82c69e00d --- /dev/null +++ b/tests/Feature/PluginSubmissionNotesTest.php @@ -0,0 +1,212 @@ + $extraComposerData + */ + private function fakeGitHubForPlugin(string $repoSlug, array $extraComposerData = []): void + { + $base = "https://api.github.com/repos/{$repoSlug}"; + $composerJson = json_encode(array_merge([ + 'name' => $repoSlug, + 'description' => "A test plugin: {$repoSlug}", + 'require' => [ + 'php' => '^8.1', + 'nativephp/mobile' => '^3.0.0', + ], + ], $extraComposerData)); + + Http::fake([ + "{$base}/contents/README.md" => Http::response([ + 'content' => base64_encode("# {$repoSlug}"), + 'encoding' => 'base64', + ]), + "{$base}/contents/composer.json" => Http::response([ + 'content' => base64_encode($composerJson), + 'encoding' => 'base64', + ]), + "{$base}/contents/nativephp.json" => Http::response([], 404), + "{$base}/contents/LICENSE*" => Http::response([], 404), + "{$base}/releases/latest" => Http::response([], 404), + "{$base}/tags*" => Http::response([]), + "https://raw.githubusercontent.com/{$repoSlug}/*" => Http::response('', 404), + $base => Http::response(['default_branch' => 'main']), + "{$base}/git/trees/main*" => Http::response([ + 'tree' => [ + ['path' => 'src/ServiceProvider.php', 'type' => 'blob'], + ], + ]), + "{$base}/readme" => Http::response([ + 'content' => base64_encode("# {$repoSlug}"), + 'encoding' => 'base64', + ]), + ]); + } + + private function createUserWithGitHub(): User + { + $user = User::factory()->create([ + 'github_id' => '12345', + ]); + DeveloperAccount::factory()->withAcceptedTerms()->create([ + 'user_id' => $user->id, + ]); + + return $user; + } + + /** @test */ + public function submitting_a_plugin_saves_notes(): void + { + Notification::fake(); + $user = $this->createUserWithGitHub(); + $repoSlug = 'acme/notes-plugin'; + $this->fakeGitHubForPlugin($repoSlug); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('repository', $repoSlug) + ->set('pluginType', 'free') + ->set('notes', 'Please review this quickly, we have a launch deadline.') + ->call('submitPlugin') + ->assertRedirect(); + + $plugin = $user->plugins()->where('repository_url', "https://github.com/{$repoSlug}")->first(); + + $this->assertNotNull($plugin); + $this->assertEquals('Please review this quickly, we have a launch deadline.', $plugin->notes); + } + + /** @test */ + public function submitting_a_plugin_without_notes_stores_null(): void + { + Notification::fake(); + $user = $this->createUserWithGitHub(); + $repoSlug = 'acme/no-notes-plugin'; + $this->fakeGitHubForPlugin($repoSlug); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('repository', $repoSlug) + ->set('pluginType', 'free') + ->call('submitPlugin') + ->assertRedirect(); + + $plugin = $user->plugins()->where('repository_url', "https://github.com/{$repoSlug}")->first(); + + $this->assertNotNull($plugin); + $this->assertNull($plugin->notes); + } + + /** @test */ + public function submitting_a_plugin_saves_support_channel_email(): void + { + Notification::fake(); + $user = $this->createUserWithGitHub(); + $repoSlug = 'acme/support-email-plugin'; + $this->fakeGitHubForPlugin($repoSlug); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('repository', $repoSlug) + ->set('pluginType', 'free') + ->set('supportChannel', 'help@example.com') + ->call('submitPlugin') + ->assertRedirect(); + + $plugin = $user->plugins()->where('repository_url', "https://github.com/{$repoSlug}")->first(); + + $this->assertNotNull($plugin); + $this->assertEquals('help@example.com', $plugin->support_channel); + } + + /** @test */ + public function submitting_a_plugin_saves_support_channel_url(): void + { + Notification::fake(); + $user = $this->createUserWithGitHub(); + $repoSlug = 'acme/support-url-plugin'; + $this->fakeGitHubForPlugin($repoSlug); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('repository', $repoSlug) + ->set('pluginType', 'free') + ->set('supportChannel', 'https://example.com/support') + ->call('submitPlugin') + ->assertRedirect(); + + $plugin = $user->plugins()->where('repository_url', "https://github.com/{$repoSlug}")->first(); + + $this->assertNotNull($plugin); + $this->assertEquals('https://example.com/support', $plugin->support_channel); + } + + /** @test */ + public function submitting_a_plugin_without_support_channel_stores_null(): void + { + Notification::fake(); + $user = $this->createUserWithGitHub(); + $repoSlug = 'acme/no-support-plugin'; + $this->fakeGitHubForPlugin($repoSlug); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('repository', $repoSlug) + ->set('pluginType', 'free') + ->call('submitPlugin') + ->assertRedirect(); + + $plugin = $user->plugins()->where('repository_url', "https://github.com/{$repoSlug}")->first(); + + $this->assertNotNull($plugin); + $this->assertNull($plugin->support_channel); + } + + /** @test */ + public function notes_and_support_channel_fields_are_hidden_until_repository_selected(): void + { + $user = User::factory()->create([ + 'github_id' => '12345', + ]); + + Livewire::actingAs($user) + ->test(Create::class) + ->assertDontSee('Support Channel') + ->assertDontSee('Any notes for the review team') + ->set('repository', 'acme/my-plugin') + ->assertSee('Support Channel') + ->assertSee('Notes'); + } + + /** @test */ + public function plugin_factory_with_notes_state_works(): void + { + $plugin = Plugin::factory()->withNotes('Custom note')->create(); + + $this->assertEquals('Custom note', $plugin->notes); + } + + /** @test */ + public function plugin_factory_with_support_channel_state_works(): void + { + $plugin = Plugin::factory()->withSupportChannel('support@myplugin.dev')->create(); + + $this->assertEquals('support@myplugin.dev', $plugin->support_channel); + } +}