Skip to content

Commit a8aeda3

Browse files
simonhampclaude
andauthored
Separate plugin creation from submission with draft status (#340)
* Separate plugin creation from submission with draft status Introduce a draft state for plugins so authors can save work-in-progress before submitting for review. Add display_name support across marketplace, cart, and checkout pages. Allow plugin owners to preview their listing. Hide de-listed plugins from the public directory. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add read-only type/tier summary for approved plugins and fix duplicate name display Show a non-editable type and pricing tier card on the approved plugin edit page. Only show the composer package name subheading when a custom display name is set, preventing the name from appearing twice. Fix preview banner test assertion to match updated text. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Improve plugin edit UX: tier validation, de-listed indicator, submit summary Add server-side validation for pricing tier when saving paid draft plugins with inline error and red card border. Show gray dot for de-listed plugins and remove Status column from plugin index. Move GitHub banner into Details tab. Add plugin summary card to Submit for Review tab. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add command to send plugin submission reminder notifications New artisan command `plugins:send-submission-reminders` emails and notifies users with unapproved plugins to finalize their submissions, listing each plugin by Composer package name. Supports --dry-run flag. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4727552 commit a8aeda3

38 files changed

Lines changed: 2042 additions & 499 deletions
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Enums\PluginStatus;
6+
use App\Models\User;
7+
use App\Notifications\PluginSubmissionReminder;
8+
use Illuminate\Console\Command;
9+
10+
class SendPluginSubmissionReminders extends Command
11+
{
12+
protected $signature = 'plugins:send-submission-reminders
13+
{--dry-run : Show what would be sent without actually sending}';
14+
15+
protected $description = 'Send a reminder to users with unapproved plugin submissions to finalize their configuration';
16+
17+
public function handle(): int
18+
{
19+
$dryRun = $this->option('dry-run');
20+
21+
if ($dryRun) {
22+
$this->info('DRY RUN - No notifications will be sent');
23+
}
24+
25+
$users = User::query()
26+
->whereHas('plugins', function ($query) {
27+
$query->whereIn('status', [PluginStatus::Draft, PluginStatus::Pending, PluginStatus::Rejected]);
28+
})
29+
->with(['plugins' => function ($query) {
30+
$query->whereIn('status', [PluginStatus::Draft, PluginStatus::Pending, PluginStatus::Rejected])
31+
->orderBy('name');
32+
}])
33+
->get();
34+
35+
$this->info("Found {$users->count()} user(s) with unapproved plugins");
36+
37+
$sent = 0;
38+
39+
foreach ($users as $user) {
40+
$pluginNames = $user->plugins->pluck('name')->join(', ');
41+
42+
if ($dryRun) {
43+
$this->line("Would send to: {$user->email} ({$user->plugins->count()} plugin(s): {$pluginNames})");
44+
} else {
45+
$user->notify(new PluginSubmissionReminder($user->plugins));
46+
$this->line("Sent to: {$user->email} ({$user->plugins->count()} plugin(s): {$pluginNames})");
47+
}
48+
49+
$sent++;
50+
}
51+
52+
$this->newLine();
53+
$this->info($dryRun ? "Would send: {$sent} notification(s)" : "Sent: {$sent} notification(s)");
54+
55+
return Command::SUCCESS;
56+
}
57+
}

app/Enums/PluginActivityType.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ enum PluginActivityType: string
99
case Approved = 'approved';
1010
case Rejected = 'rejected';
1111
case DescriptionUpdated = 'description_updated';
12+
case Withdrawn = 'withdrawn';
13+
case ReturnedToDraft = 'returned_to_draft';
1214

1315
public function label(): string
1416
{
@@ -18,6 +20,8 @@ public function label(): string
1820
self::Approved => 'Approved',
1921
self::Rejected => 'Rejected',
2022
self::DescriptionUpdated => 'Description Updated',
23+
self::Withdrawn => 'Withdrawn',
24+
self::ReturnedToDraft => 'Returned to Draft',
2125
};
2226
}
2327

@@ -29,6 +33,8 @@ public function color(): string
2933
self::Approved => 'success',
3034
self::Rejected => 'danger',
3135
self::DescriptionUpdated => 'gray',
36+
self::Withdrawn => 'warning',
37+
self::ReturnedToDraft => 'warning',
3238
};
3339
}
3440

@@ -40,6 +46,8 @@ public function icon(): string
4046
self::Approved => 'heroicon-o-check-circle',
4147
self::Rejected => 'heroicon-o-x-circle',
4248
self::DescriptionUpdated => 'heroicon-o-pencil-square',
49+
self::Withdrawn => 'heroicon-o-arrow-uturn-left',
50+
self::ReturnedToDraft => 'heroicon-o-arrow-uturn-left',
4351
};
4452
}
4553
}

app/Enums/PluginStatus.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44

55
enum PluginStatus: string
66
{
7+
case Draft = 'draft';
78
case Pending = 'pending';
89
case Approved = 'approved';
910
case Rejected = 'rejected';
1011

1112
public function label(): string
1213
{
1314
return match ($this) {
15+
self::Draft => 'Draft',
1416
self::Pending => 'Pending Review',
1517
self::Approved => 'Approved',
1618
self::Rejected => 'Rejected',
@@ -20,6 +22,7 @@ public function label(): string
2022
public function color(): string
2123
{
2224
return match ($this) {
25+
self::Draft => 'zinc',
2326
self::Pending => 'yellow',
2427
self::Approved => 'green',
2528
self::Rejected => 'red',

app/Filament/Resources/PluginResource.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ public static function table(Table $table): Table
247247
Tables\Columns\TextColumn::make('status')
248248
->badge()
249249
->color(fn (PluginStatus $state): string => match ($state) {
250+
PluginStatus::Draft => 'gray',
250251
PluginStatus::Pending => 'warning',
251252
PluginStatus::Approved => 'success',
252253
PluginStatus::Rejected => 'danger',

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
namespace App\Filament\Resources\PluginResource\Pages;
44

5+
use App\Enums\PluginStatus;
56
use App\Filament\Resources\PluginResource;
67
use Filament\Actions;
78
use Filament\Resources\Pages\ListRecords;
9+
use Illuminate\Database\Eloquent\Builder;
810

911
class ListPlugins extends ListRecords
1012
{
@@ -16,4 +18,10 @@ protected function getHeaderActions(): array
1618
Actions\CreateAction::make(),
1719
];
1820
}
21+
22+
protected function getTableQuery(): ?Builder
23+
{
24+
return parent::getTableQuery()
25+
->where('status', '!=', PluginStatus::Draft);
26+
}
1927
}

app/Http/Controllers/CartController.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@ public function status(Request $request, string $sessionId): JsonResponse
421421
'id' => $license->id,
422422
'plugin_id' => $license->plugin->id,
423423
'plugin_name' => $license->plugin->name,
424+
'plugin_display_name' => $license->plugin->display_name,
424425
'plugin_slug' => $license->plugin->slug,
425426
]),
426427
'products' => $productLicenses->map(fn ($license) => [

app/Http/Controllers/PluginDirectoryController.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public function index(): View
1515

1616
$featuredPlugins = Plugin::query()
1717
->approved()
18+
->where('is_active', true)
1819
->featured()
1920
->latest()
2021
->take(16)
@@ -24,6 +25,7 @@ public function index(): View
2425

2526
$latestPlugins = Plugin::query()
2627
->approved()
28+
->where('is_active', true)
2729
->where('featured', false)
2830
->latest()
2931
->take(16)
@@ -52,11 +54,12 @@ public function show(string $vendor, string $package): View
5254
$user = Auth::user();
5355

5456
$isAdmin = $user?->isAdmin() ?? false;
57+
$isOwner = $user && $plugin->user_id === $user->id;
5558

56-
abort_unless($plugin->isApproved() || $isAdmin, 404);
59+
abort_unless(($plugin->isApproved() && $plugin->is_active) || $isAdmin || $isOwner, 404);
5760

58-
// For paid plugins, check if user has an accessible price (admins bypass)
59-
if (! $isAdmin && $plugin->isPaid() && ! $plugin->hasAccessiblePriceFor($user)) {
61+
// For paid plugins, check if user has an accessible price (admins and owners bypass)
62+
if (! $isAdmin && ! $isOwner && $plugin->isPaid() && ! $plugin->hasAccessiblePriceFor($user)) {
6063
abort(404);
6164
}
6265

@@ -74,7 +77,7 @@ public function show(string $vendor, string $package): View
7477
'bestPrice' => $bestPrice,
7578
'regularPrice' => $regularPrice,
7679
'hasDiscount' => $bestPrice && $regularPrice && $bestPrice->id !== $regularPrice->id,
77-
'isAdminPreview' => ! $plugin->isApproved(),
80+
'isAdminPreview' => (! $plugin->isApproved() || ! $plugin->is_active) && ($isAdmin || $isOwner),
7881
]);
7982
}
8083

app/Livewire/Customer/Plugins/Create.php

Lines changed: 6 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44

55
use App\Enums\PluginStatus;
66
use App\Features\AllowPaidPlugins;
7-
use App\Jobs\ReviewPluginRepository;
87
use App\Models\Plugin;
9-
use App\Notifications\PluginSubmitted;
108
use App\Services\GitHubUserService;
119
use App\Services\PluginSyncService;
1210
use Illuminate\Support\Facades\Cache;
@@ -17,7 +15,7 @@
1715
use Livewire\Component;
1816

1917
#[Layout('components.layouts.dashboard')]
20-
#[Title('Submit Your Plugin')]
18+
#[Title('Create Your Plugin')]
2119
class Create extends Component
2220
{
2321
public string $pluginType = 'free';
@@ -26,10 +24,6 @@ class Create extends Component
2624

2725
public string $repository = '';
2826

29-
public string $notes = '';
30-
31-
public string $supportChannel = '';
32-
3327
/** @var array<int, array{id: int, full_name: string, name: string, owner: string, private: bool}> */
3428
public array $repositories = [];
3529

@@ -113,12 +107,12 @@ public function loadRepositories(): void
113107
$this->loadingRepos = false;
114108
}
115109

116-
public function submitPlugin(PluginSyncService $syncService): void
110+
public function createPlugin(PluginSyncService $syncService): void
117111
{
118112
$user = auth()->user();
119113

120114
if (! $user->github_id) {
121-
$this->addError('repository', 'You must connect your GitHub account to submit a plugin.');
115+
$this->addError('repository', 'You must connect your GitHub account to create a plugin.');
122116

123117
return;
124118
}
@@ -132,26 +126,14 @@ public function submitPlugin(PluginSyncService $syncService): void
132126
function ($attribute, $value, $fail): void {
133127
$url = 'https://github.com/'.trim($value, '/');
134128
if (Plugin::where('repository_url', $url)->exists()) {
135-
$fail('This repository has already been submitted.');
129+
$fail('A plugin for this repository already exists.');
136130
}
137131
},
138132
],
139133
'pluginType' => ['required', 'string', 'in:free,paid'],
140-
'notes' => ['nullable', 'string', 'max:5000'],
141-
'supportChannel' => [
142-
'required',
143-
'string',
144-
'max:255',
145-
function (string $attribute, mixed $value, \Closure $fail) {
146-
if (! filter_var($value, FILTER_VALIDATE_EMAIL) && ! filter_var($value, FILTER_VALIDATE_URL)) {
147-
$fail('The support channel must be a valid email address or URL.');
148-
}
149-
},
150-
],
151134
], [
152135
'repository.required' => 'Please select a repository for your plugin.',
153136
'repository.regex' => 'Please enter a valid repository in the format vendor/repo-name.',
154-
'supportChannel.required' => 'Please provide a support channel (email or URL) for your plugin.',
155137
]);
156138

157139
if ($this->pluginType === 'paid' && ! Feature::active(AllowPaidPlugins::class)) {
@@ -195,31 +177,12 @@ function (string $attribute, mixed $value, \Closure $fail) {
195177
$plugin = $user->plugins()->create([
196178
'repository_url' => $repositoryUrl,
197179
'type' => $this->pluginType,
198-
'status' => PluginStatus::Pending,
180+
'status' => PluginStatus::Draft,
199181
'developer_account_id' => $developerAccountId,
200-
'notes' => $this->notes ?: null,
201-
'support_channel' => $this->supportChannel ?: null,
202182
]);
203183

204-
$webhookSecret = $plugin->generateWebhookSecret();
205-
206-
$webhookInstalled = false;
207-
if ($user->hasGitHubToken()) {
208-
$webhookResult = $githubService->createWebhook(
209-
$owner,
210-
$repo,
211-
$plugin->getWebhookUrl(),
212-
$webhookSecret
213-
);
214-
$webhookInstalled = $webhookResult['success'];
215-
}
216-
217-
$plugin->update(['webhook_installed' => $webhookInstalled]);
218-
219184
$syncService->sync($plugin);
220185

221-
(new ReviewPluginRepository($plugin))->handle();
222-
223186
if (! $plugin->name) {
224187
$plugin->delete();
225188

@@ -228,21 +191,14 @@ function (string $attribute, mixed $value, \Closure $fail) {
228191
return;
229192
}
230193

231-
$user->notify(new PluginSubmitted($plugin));
232-
233-
$successMessage = 'Your plugin has been submitted for review!';
234-
if (! $webhookInstalled) {
235-
$successMessage .= ' Please set up the webhook manually to enable automatic syncing.';
236-
}
237-
238194
[$vendor, $package] = explode('/', $plugin->name);
239195

240196
$this->redirect(
241197
route('customer.plugins.show', ['vendor' => $vendor, 'package' => $package]),
242198
navigate: true
243199
);
244200

245-
session()->flash('success', $successMessage);
201+
session()->flash('success', 'Your plugin has been created as a draft. You can edit it and submit for review when ready.');
246202
}
247203

248204
public function render()

app/Livewire/Customer/Plugins/Index.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
class Index extends Component
1717
{
1818
#[Url]
19-
public string $status = 'pending';
19+
public string $status = 'draft';
2020

2121
#[Computed]
2222
public function plugins(): Collection
@@ -37,6 +37,7 @@ public function pluginCounts(): array
3737
->toArray();
3838

3939
return [
40+
PluginStatus::Draft->value => $counts[PluginStatus::Draft->value] ?? 0,
4041
PluginStatus::Approved->value => $counts[PluginStatus::Approved->value] ?? 0,
4142
PluginStatus::Pending->value => $counts[PluginStatus::Pending->value] ?? 0,
4243
PluginStatus::Rejected->value => $counts[PluginStatus::Rejected->value] ?? 0,

0 commit comments

Comments
 (0)