Skip to content

Commit e77aa06

Browse files
simonhampclaude
andauthored
Add notes and support channel fields to plugin submissions (#303)
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 <noreply@anthropic.com>
1 parent 105178f commit e77aa06

File tree

6 files changed

+302
-1
lines changed

6 files changed

+302
-1
lines changed

app/Filament/Resources/PluginResource.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,18 @@ public static function form(Schema $schema): Schema
154154
])
155155
->visible(fn (?Plugin $record) => $record?->review_checks !== null),
156156

157+
Schemas\Components\Section::make('Developer Submission Details')
158+
->schema([
159+
Forms\Components\Placeholder::make('support_channel_display')
160+
->label('Support Channel')
161+
->content(fn (?Plugin $record) => $record?->support_channel ?? 'Not provided'),
162+
163+
Forms\Components\Placeholder::make('notes_display')
164+
->label('Notes')
165+
->content(fn (?Plugin $record) => $record?->notes ?? 'Not provided'),
166+
])
167+
->visible(fn (?Plugin $record) => $record !== null),
168+
157169
Schemas\Components\Section::make('Submission Info')
158170
->schema([
159171
Forms\Components\Select::make('user_id')

app/Livewire/Customer/Plugins/Create.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ class Create extends Component
2222

2323
public string $repository = '';
2424

25+
public string $notes = '';
26+
27+
public string $supportChannel = '';
28+
2529
/** @var array<int, array{id: int, full_name: string, private: bool}> */
2630
public array $repositories = [];
2731

@@ -90,6 +94,8 @@ function ($attribute, $value, $fail): void {
9094
},
9195
],
9296
'pluginType' => ['required', 'string', 'in:free,paid'],
97+
'notes' => ['nullable', 'string', 'max:5000'],
98+
'supportChannel' => ['nullable', 'string', 'max:255'],
9399
], [
94100
'repository.required' => 'Please select a repository for your plugin.',
95101
'repository.regex' => 'Please enter a valid repository in the format vendor/repo-name.',
@@ -115,6 +121,8 @@ function ($attribute, $value, $fail): void {
115121
'type' => $this->pluginType,
116122
'status' => PluginStatus::Pending,
117123
'developer_account_id' => $developerAccountId,
124+
'notes' => $this->notes ?: null,
125+
'support_channel' => $this->supportChannel ?: null,
118126
]);
119127

120128
$webhookSecret = $plugin->generateWebhookSecret();

database/factories/PluginFactory.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,18 @@ public function withoutDescription(): static
185185
'description' => null,
186186
]);
187187
}
188+
189+
public function withNotes(?string $notes = null): static
190+
{
191+
return $this->state(fn (array $attributes) => [
192+
'notes' => $notes ?? fake()->sentence(),
193+
]);
194+
}
195+
196+
public function withSupportChannel(?string $channel = null): static
197+
{
198+
return $this->state(fn (array $attributes) => [
199+
'support_channel' => $channel ?? fake()->safeEmail(),
200+
]);
201+
}
188202
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*/
12+
public function up(): void
13+
{
14+
Schema::table('plugins', function (Blueprint $table) {
15+
$table->text('notes')->nullable()->after('description');
16+
$table->string('support_channel')->nullable()->after('notes');
17+
});
18+
}
19+
20+
/**
21+
* Reverse the migrations.
22+
*/
23+
public function down(): void
24+
{
25+
Schema::table('plugins', function (Blueprint $table) {
26+
$table->dropColumn(['notes', 'support_channel']);
27+
});
28+
}
29+
};

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@
112112
<span>Loading repositories...</span>
113113
</div>
114114
@elseif($reposLoaded)
115-
<flux:select wire:model="repository" label="Repository" placeholder="Select a repository...">
115+
<flux:select wire:model.live="repository" label="Repository" placeholder="Select a repository...">
116116
@foreach($repositories as $repo)
117117
<flux:select.option value="{{ $repo['full_name'] }}">
118118
{{ $repo['full_name'] }}{{ $repo['private'] ? ' (private)' : '' }}
@@ -153,6 +153,32 @@
153153
@endif
154154
@endfeature
155155

156+
@if($repository)
157+
{{-- Support Channel --}}
158+
<flux:card>
159+
<flux:heading size="lg">Support Channel</flux:heading>
160+
<flux:text class="mt-1">
161+
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.
162+
</flux:text>
163+
164+
<div class="mt-6">
165+
<flux:input wire:model="supportChannel" label="Support Channel" placeholder="support@example.com or https://..." />
166+
</div>
167+
</flux:card>
168+
169+
{{-- Notes --}}
170+
<flux:card>
171+
<flux:heading size="lg">Notes</flux:heading>
172+
<flux:text class="mt-1">
173+
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.
174+
</flux:text>
175+
176+
<div class="mt-6">
177+
<flux:textarea wire:model="notes" label="Notes" placeholder="Optional notes for the review team..." rows="4" />
178+
</div>
179+
</flux:card>
180+
@endif
181+
156182
{{-- Submit Button --}}
157183
<div class="flex items-center justify-end gap-4">
158184
<flux:button variant="ghost" href="{{ route('customer.plugins.index') }}">Cancel</flux:button>
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
<?php
2+
3+
namespace Tests\Feature;
4+
5+
use App\Livewire\Customer\Plugins\Create;
6+
use App\Models\DeveloperAccount;
7+
use App\Models\Plugin;
8+
use App\Models\User;
9+
use Illuminate\Foundation\Testing\RefreshDatabase;
10+
use Illuminate\Support\Facades\Http;
11+
use Illuminate\Support\Facades\Notification;
12+
use Livewire\Livewire;
13+
use Tests\TestCase;
14+
15+
class PluginSubmissionNotesTest extends TestCase
16+
{
17+
use RefreshDatabase;
18+
19+
/**
20+
* @param array<string, mixed> $extraComposerData
21+
*/
22+
private function fakeGitHubForPlugin(string $repoSlug, array $extraComposerData = []): void
23+
{
24+
$base = "https://api.github.com/repos/{$repoSlug}";
25+
$composerJson = json_encode(array_merge([
26+
'name' => $repoSlug,
27+
'description' => "A test plugin: {$repoSlug}",
28+
'require' => [
29+
'php' => '^8.1',
30+
'nativephp/mobile' => '^3.0.0',
31+
],
32+
], $extraComposerData));
33+
34+
Http::fake([
35+
"{$base}/contents/README.md" => Http::response([
36+
'content' => base64_encode("# {$repoSlug}"),
37+
'encoding' => 'base64',
38+
]),
39+
"{$base}/contents/composer.json" => Http::response([
40+
'content' => base64_encode($composerJson),
41+
'encoding' => 'base64',
42+
]),
43+
"{$base}/contents/nativephp.json" => Http::response([], 404),
44+
"{$base}/contents/LICENSE*" => Http::response([], 404),
45+
"{$base}/releases/latest" => Http::response([], 404),
46+
"{$base}/tags*" => Http::response([]),
47+
"https://raw.githubusercontent.com/{$repoSlug}/*" => Http::response('', 404),
48+
$base => Http::response(['default_branch' => 'main']),
49+
"{$base}/git/trees/main*" => Http::response([
50+
'tree' => [
51+
['path' => 'src/ServiceProvider.php', 'type' => 'blob'],
52+
],
53+
]),
54+
"{$base}/readme" => Http::response([
55+
'content' => base64_encode("# {$repoSlug}"),
56+
'encoding' => 'base64',
57+
]),
58+
]);
59+
}
60+
61+
private function createUserWithGitHub(): User
62+
{
63+
$user = User::factory()->create([
64+
'github_id' => '12345',
65+
]);
66+
DeveloperAccount::factory()->withAcceptedTerms()->create([
67+
'user_id' => $user->id,
68+
]);
69+
70+
return $user;
71+
}
72+
73+
/** @test */
74+
public function submitting_a_plugin_saves_notes(): void
75+
{
76+
Notification::fake();
77+
$user = $this->createUserWithGitHub();
78+
$repoSlug = 'acme/notes-plugin';
79+
$this->fakeGitHubForPlugin($repoSlug);
80+
81+
Livewire::actingAs($user)
82+
->test(Create::class)
83+
->set('repository', $repoSlug)
84+
->set('pluginType', 'free')
85+
->set('notes', 'Please review this quickly, we have a launch deadline.')
86+
->call('submitPlugin')
87+
->assertRedirect();
88+
89+
$plugin = $user->plugins()->where('repository_url', "https://github.com/{$repoSlug}")->first();
90+
91+
$this->assertNotNull($plugin);
92+
$this->assertEquals('Please review this quickly, we have a launch deadline.', $plugin->notes);
93+
}
94+
95+
/** @test */
96+
public function submitting_a_plugin_without_notes_stores_null(): void
97+
{
98+
Notification::fake();
99+
$user = $this->createUserWithGitHub();
100+
$repoSlug = 'acme/no-notes-plugin';
101+
$this->fakeGitHubForPlugin($repoSlug);
102+
103+
Livewire::actingAs($user)
104+
->test(Create::class)
105+
->set('repository', $repoSlug)
106+
->set('pluginType', 'free')
107+
->call('submitPlugin')
108+
->assertRedirect();
109+
110+
$plugin = $user->plugins()->where('repository_url', "https://github.com/{$repoSlug}")->first();
111+
112+
$this->assertNotNull($plugin);
113+
$this->assertNull($plugin->notes);
114+
}
115+
116+
/** @test */
117+
public function submitting_a_plugin_saves_support_channel_email(): void
118+
{
119+
Notification::fake();
120+
$user = $this->createUserWithGitHub();
121+
$repoSlug = 'acme/support-email-plugin';
122+
$this->fakeGitHubForPlugin($repoSlug);
123+
124+
Livewire::actingAs($user)
125+
->test(Create::class)
126+
->set('repository', $repoSlug)
127+
->set('pluginType', 'free')
128+
->set('supportChannel', 'help@example.com')
129+
->call('submitPlugin')
130+
->assertRedirect();
131+
132+
$plugin = $user->plugins()->where('repository_url', "https://github.com/{$repoSlug}")->first();
133+
134+
$this->assertNotNull($plugin);
135+
$this->assertEquals('help@example.com', $plugin->support_channel);
136+
}
137+
138+
/** @test */
139+
public function submitting_a_plugin_saves_support_channel_url(): void
140+
{
141+
Notification::fake();
142+
$user = $this->createUserWithGitHub();
143+
$repoSlug = 'acme/support-url-plugin';
144+
$this->fakeGitHubForPlugin($repoSlug);
145+
146+
Livewire::actingAs($user)
147+
->test(Create::class)
148+
->set('repository', $repoSlug)
149+
->set('pluginType', 'free')
150+
->set('supportChannel', 'https://example.com/support')
151+
->call('submitPlugin')
152+
->assertRedirect();
153+
154+
$plugin = $user->plugins()->where('repository_url', "https://github.com/{$repoSlug}")->first();
155+
156+
$this->assertNotNull($plugin);
157+
$this->assertEquals('https://example.com/support', $plugin->support_channel);
158+
}
159+
160+
/** @test */
161+
public function submitting_a_plugin_without_support_channel_stores_null(): void
162+
{
163+
Notification::fake();
164+
$user = $this->createUserWithGitHub();
165+
$repoSlug = 'acme/no-support-plugin';
166+
$this->fakeGitHubForPlugin($repoSlug);
167+
168+
Livewire::actingAs($user)
169+
->test(Create::class)
170+
->set('repository', $repoSlug)
171+
->set('pluginType', 'free')
172+
->call('submitPlugin')
173+
->assertRedirect();
174+
175+
$plugin = $user->plugins()->where('repository_url', "https://github.com/{$repoSlug}")->first();
176+
177+
$this->assertNotNull($plugin);
178+
$this->assertNull($plugin->support_channel);
179+
}
180+
181+
/** @test */
182+
public function notes_and_support_channel_fields_are_hidden_until_repository_selected(): void
183+
{
184+
$user = User::factory()->create([
185+
'github_id' => '12345',
186+
]);
187+
188+
Livewire::actingAs($user)
189+
->test(Create::class)
190+
->assertDontSee('Support Channel')
191+
->assertDontSee('Any notes for the review team')
192+
->set('repository', 'acme/my-plugin')
193+
->assertSee('Support Channel')
194+
->assertSee('Notes');
195+
}
196+
197+
/** @test */
198+
public function plugin_factory_with_notes_state_works(): void
199+
{
200+
$plugin = Plugin::factory()->withNotes('Custom note')->create();
201+
202+
$this->assertEquals('Custom note', $plugin->notes);
203+
}
204+
205+
/** @test */
206+
public function plugin_factory_with_support_channel_state_works(): void
207+
{
208+
$plugin = Plugin::factory()->withSupportChannel('support@myplugin.dev')->create();
209+
210+
$this->assertEquals('support@myplugin.dev', $plugin->support_channel);
211+
}
212+
}

0 commit comments

Comments
 (0)