Skip to content

Commit 6660f52

Browse files
shanerbaner82claude
andcommitted
Add min_version review checks and table quick actions
Review checks now fetch nativephp.json and verify ios.min_version and android.min_version are present (7 checks total). Added resync and review checks as grouped table row actions for quick access from the plugin list. Updated the Review Checks form section to display the new fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e8d0ce0 commit 6660f52

File tree

4 files changed

+210
-28
lines changed

4 files changed

+210
-28
lines changed

app/Filament/Resources/PluginResource.php

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
use App\Enums\PluginType;
88
use App\Filament\Resources\PluginResource\Pages;
99
use App\Filament\Resources\PluginResource\RelationManagers;
10+
use App\Jobs\ReviewPluginRepository;
11+
use App\Jobs\SyncPlugin;
1012
use App\Models\Plugin;
1113
use Filament\Forms;
1214
use Filament\Forms\Form;
15+
use Filament\Notifications\Notification;
1316
use Filament\Resources\Resource;
1417
use Filament\Tables;
1518
use Filament\Tables\Table;
@@ -103,8 +106,20 @@ public static function form(Form $form): Form
103106
->content(fn (?Plugin $record) => ($record?->review_checks['requires_mobile_sdk'] ?? false)
104107
? ''.($record->review_checks['mobile_sdk_constraint'] ?? '')
105108
: '❌ Missing'),
109+
110+
Forms\Components\Placeholder::make('review_ios_min_version')
111+
->label('iOS min_version')
112+
->content(fn (?Plugin $record) => ($record?->review_checks['has_ios_min_version'] ?? false)
113+
? ''.($record->review_checks['ios_min_version'] ?? '')
114+
: '❌ Missing'),
115+
116+
Forms\Components\Placeholder::make('review_android_min_version')
117+
->label('Android min_version')
118+
->content(fn (?Plugin $record) => ($record?->review_checks['has_android_min_version'] ?? false)
119+
? ''.($record->review_checks['android_min_version'] ?? '')
120+
: '❌ Missing'),
106121
])
107-
->columns(3)
122+
->columns(4)
108123
->visible(fn (?Plugin $record) => $record?->review_checks !== null),
109124

110125
Forms\Components\Section::make('Submission Info')
@@ -212,6 +227,62 @@ public static function table(Table $table): Table
212227
->label('Active'),
213228
])
214229
->actions([
230+
Tables\Actions\ActionGroup::make([
231+
Tables\Actions\Action::make('resync')
232+
->label('Re-sync from GitHub')
233+
->icon('heroicon-o-arrow-path')
234+
->color('primary')
235+
->visible(fn (Plugin $record): bool => $record->repository_url !== null)
236+
->requiresConfirmation()
237+
->modalHeading('Re-sync Plugin')
238+
->modalDescription(fn (Plugin $record): string => "This will re-fetch the README, composer.json, nativephp.json, license, and latest version from GitHub for '{$record->name}'.")
239+
->action(function (Plugin $record): void {
240+
SyncPlugin::dispatch($record);
241+
242+
Notification::make()
243+
->title('Sync queued')
244+
->body("A background sync has been queued for '{$record->name}'.")
245+
->success()
246+
->send();
247+
}),
248+
249+
Tables\Actions\Action::make('runReviewChecks')
250+
->label('Run Review Checks')
251+
->icon('heroicon-o-clipboard-document-check')
252+
->color('primary')
253+
->visible(fn (Plugin $record): bool => $record->repository_url !== null)
254+
->requiresConfirmation()
255+
->modalHeading('Run Review Checks')
256+
->modalDescription(fn (Plugin $record): string => "This will run automated checks for '{$record->name}'.")
257+
->action(function (Plugin $record): void {
258+
$checks = (new ReviewPluginRepository($record))->handle();
259+
260+
if (empty($checks)) {
261+
Notification::make()
262+
->title('Review checks failed')
263+
->body('Could not fetch repository data.')
264+
->danger()
265+
->send();
266+
267+
return;
268+
}
269+
270+
$passed = collect($checks)->only([
271+
'supports_ios', 'supports_android', 'supports_js',
272+
'has_support_email', 'requires_mobile_sdk',
273+
'has_ios_min_version', 'has_android_min_version',
274+
])->filter()->count();
275+
276+
Notification::make()
277+
->title("Review checks complete ({$passed}/7 passed)")
278+
->success()
279+
->send();
280+
}),
281+
])
282+
->icon('heroicon-o-bolt')
283+
->color('primary')
284+
->tooltip('Quick Actions'),
285+
215286
Tables\Actions\EditAction::make(),
216287
])
217288
->bulkActions([

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

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,32 +27,32 @@ protected function getHeaderActions(): array
2727
Actions\Action::make('approve')
2828
->icon('heroicon-o-check')
2929
->color('success')
30-
->visible(fn() => $this->record->isPending())
31-
->action(fn() => $this->record->approve(auth()->id()))
30+
->visible(fn () => $this->record->isPending())
31+
->action(fn () => $this->record->approve(auth()->id()))
3232
->requiresConfirmation()
3333
->modalHeading('Approve Plugin')
34-
->modalDescription(fn() => "Are you sure you want to approve '{$this->record->name}'?"),
34+
->modalDescription(fn () => "Are you sure you want to approve '{$this->record->name}'?"),
3535

3636
Actions\Action::make('reject')
3737
->icon('heroicon-o-x-mark')
3838
->color('danger')
39-
->visible(fn() => $this->record->isPending() || $this->record->isApproved())
39+
->visible(fn () => $this->record->isPending() || $this->record->isApproved())
4040
->form([
4141
Forms\Components\Textarea::make('rejection_reason')
4242
->label('Reason for Rejection')
4343
->required()
4444
->rows(3)
4545
->placeholder('Please explain why this plugin is being rejected...'),
4646
])
47-
->action(fn(array $data) => $this->record->reject($data['rejection_reason'], auth()->id()))
47+
->action(fn (array $data) => $this->record->reject($data['rejection_reason'], auth()->id()))
4848
->modalHeading('Reject Plugin')
49-
->modalDescription(fn() => "Are you sure you want to reject '{$this->record->name}'?"),
49+
->modalDescription(fn () => "Are you sure you want to reject '{$this->record->name}'?"),
5050

5151
Actions\Action::make('convertToPaid')
5252
->label('Convert to Paid')
5353
->icon('heroicon-o-currency-dollar')
5454
->color('success')
55-
->visible(fn() => $this->record->isFree())
55+
->visible(fn () => $this->record->isFree())
5656
->form([
5757
Forms\Components\Select::make('tier')
5858
->label('Pricing Tier')
@@ -75,17 +75,17 @@ protected function getHeaderActions(): array
7575
->send();
7676
})
7777
->modalHeading('Convert Plugin to Paid')
78-
->modalDescription(fn() => "This will convert '{$this->record->name}' from free to paid, set up pricing, and trigger a Satis build so it's available via Composer.")
78+
->modalDescription(fn () => "This will convert '{$this->record->name}' from free to paid, set up pricing, and trigger a Satis build so it's available via Composer.")
7979
->modalSubmitActionLabel('Convert & Ingest'),
8080

8181
Actions\Action::make('syncToSatis')
82-
->label(fn() => $this->record->isSatisSynced() ? 'Re-sync to Satis' : 'Sync to Satis')
82+
->label(fn () => $this->record->isSatisSynced() ? 'Re-sync to Satis' : 'Sync to Satis')
8383
->icon('heroicon-o-arrow-path')
8484
->color('warning')
85-
->visible(fn() => $this->record->isPaid())
85+
->visible(fn () => $this->record->isPaid())
8686
->requiresConfirmation()
87-
->modalHeading(fn() => $this->record->isSatisSynced() ? 'Re-sync to Satis' : 'Sync to Satis')
88-
->modalDescription(fn() => $this->record->isSatisSynced()
87+
->modalHeading(fn () => $this->record->isSatisSynced() ? 'Re-sync to Satis' : 'Sync to Satis')
88+
->modalDescription(fn () => $this->record->isSatisSynced()
8989
? "Last synced: {$this->record->satis_synced_at->diffForHumans()}. This will trigger a new Satis build for '{$this->record->name}'."
9090
: "This will trigger a Satis build for '{$this->record->name}' so it's available via Composer.")
9191
->action(function (): void {
@@ -112,7 +112,7 @@ protected function getHeaderActions(): array
112112
->orWhere('email', 'like', "%{$search}%")
113113
->limit(50)
114114
->get()
115-
->mapWithKeys(fn(User $user) => [$user->id => "{$user->name} ({$user->email})"])
115+
->mapWithKeys(fn (User $user) => [$user->id => "{$user->name} ({$user->email})"])
116116
->toArray();
117117
})
118118
->required(),
@@ -150,33 +150,33 @@ protected function getHeaderActions(): array
150150
->send();
151151
})
152152
->modalHeading('Grant Plugin to User')
153-
->modalDescription(fn() => "Grant '{$this->record->name}' to a user for free.")
153+
->modalDescription(fn () => "Grant '{$this->record->name}' to a user for free.")
154154
->modalSubmitActionLabel('Grant'),
155155

156156
Actions\Action::make('viewListing')
157157
->label('View Listing Page')
158158
->icon('heroicon-o-eye')
159159
->color('gray')
160-
->url(fn() => route('plugins.show', $this->record->routeParams()))
160+
->url(fn () => route('plugins.show', $this->record->routeParams()))
161161
->openUrlInNewTab()
162-
->visible(fn() => $this->record->isApproved()),
162+
->visible(fn () => $this->record->isApproved()),
163163

164164
Actions\Action::make('viewPackagist')
165165
->label('View on Packagist')
166166
->icon('heroicon-o-arrow-top-right-on-square')
167167
->color('gray')
168-
->url(fn() => $this->record->getPackagistUrl())
168+
->url(fn () => $this->record->getPackagistUrl())
169169
->openUrlInNewTab()
170-
->visible(fn() => $this->record->isFree()),
170+
->visible(fn () => $this->record->isFree()),
171171

172172
Actions\Action::make('runReviewChecks')
173173
->label('Run Review Checks')
174174
->icon('heroicon-o-clipboard-document-check')
175175
->color('primary')
176-
->visible(fn() => $this->record->repository_url !== null)
176+
->visible(fn () => $this->record->repository_url !== null)
177177
->requiresConfirmation()
178178
->modalHeading('Run Review Checks')
179-
->modalDescription(fn() => "This will fetch the repository tree, README, and composer.json for '{$this->record->name}' and run automated checks.")
179+
->modalDescription(fn () => "This will fetch the repository tree, README, and composer.json for '{$this->record->name}' and run automated checks.")
180180
->action(function (): void {
181181
$checks = (new ReviewPluginRepository($this->record))->handle();
182182

@@ -196,6 +196,8 @@ protected function getHeaderActions(): array
196196
['JS support', $checks['supports_js']],
197197
['Support email', $checks['has_support_email'] ? $checks['support_email'] : false],
198198
['Requires nativephp/mobile', $checks['requires_mobile_sdk'] ? $checks['mobile_sdk_constraint'] : false],
199+
['iOS min_version', $checks['has_ios_min_version'] ? $checks['ios_min_version'] : false],
200+
['Android min_version', $checks['has_android_min_version'] ? $checks['android_min_version'] : false],
199201
])->map(function (array $item): string {
200202
[$label, $value] = $item;
201203
if ($value === true) {
@@ -211,24 +213,25 @@ protected function getHeaderActions(): array
211213
$passed = collect($checks)->only([
212214
'supports_ios', 'supports_android', 'supports_js',
213215
'has_support_email', 'requires_mobile_sdk',
216+
'has_ios_min_version', 'has_android_min_version',
214217
])->filter()->count();
215218

216219
Notification::make()
217-
->title("Review checks complete ({$passed}/5 passed)")
220+
->title("Review checks complete ({$passed}/7 passed)")
218221
->body(new HtmlString($lines))
219222
->duration(15000)
220-
->color($passed === 5 ? 'success' : 'warning')
223+
->color($passed === 7 ? 'success' : 'warning')
221224
->send();
222225
}),
223226

224227
Actions\Action::make('resync')
225228
->label('Re-sync from GitHub')
226229
->icon('heroicon-o-arrow-path')
227230
->color('primary')
228-
->visible(fn() => $this->record->repository_url !== null)
231+
->visible(fn () => $this->record->repository_url !== null)
229232
->requiresConfirmation()
230233
->modalHeading('Re-sync Plugin')
231-
->modalDescription(fn() => "This will re-fetch the README, composer.json, nativephp.json, license, and latest version from GitHub for '{$this->record->name}'.")
234+
->modalDescription(fn () => "This will re-fetch the README, composer.json, nativephp.json, license, and latest version from GitHub for '{$this->record->name}'.")
232235
->action(function (): void {
233236
SyncPlugin::dispatch($this->record);
234237

@@ -243,7 +246,7 @@ protected function getHeaderActions(): array
243246
->label('View on GitHub')
244247
->icon('heroicon-o-arrow-top-right-on-square')
245248
->color('gray')
246-
->url(fn() => $this->record->getGithubUrl())
249+
->url(fn () => $this->record->getGithubUrl())
247250
->openUrlInNewTab(),
248251
])
249252
->icon('heroicon-m-ellipsis-vertical'),

app/Jobs/ReviewPluginRepository.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public function handle(): array
5050
$tree = $this->fetchRepoTree($owner, $repoName, $defaultBranch, $token);
5151
$readme = $this->fetchReadme($owner, $repoName, $token);
5252
$composerJson = $this->fetchComposerJson($owner, $repoName, $token);
53+
$nativephpJson = $this->fetchNativephpJson($owner, $repoName, $token);
5354

5455
$checks = [
5556
'supports_ios' => $this->checkDirectoryHasFiles($tree, 'resources/ios/'),
@@ -59,6 +60,10 @@ public function handle(): array
5960
'support_email' => null,
6061
'requires_mobile_sdk' => false,
6162
'mobile_sdk_constraint' => null,
63+
'has_ios_min_version' => false,
64+
'ios_min_version' => null,
65+
'has_android_min_version' => false,
66+
'android_min_version' => null,
6267
];
6368

6469
if ($readme) {
@@ -73,6 +78,16 @@ public function handle(): array
7378
$checks['mobile_sdk_constraint'] = $mobileConstraint;
7479
}
7580

81+
if ($nativephpJson) {
82+
$iosMinVersion = $nativephpJson['ios']['min_version'] ?? null;
83+
$checks['has_ios_min_version'] = $iosMinVersion !== null;
84+
$checks['ios_min_version'] = $iosMinVersion;
85+
86+
$androidMinVersion = $nativephpJson['android']['min_version'] ?? null;
87+
$checks['has_android_min_version'] = $androidMinVersion !== null;
88+
$checks['android_min_version'] = $androidMinVersion;
89+
}
90+
7691
$this->plugin->update([
7792
'review_checks' => $checks,
7893
'reviewed_at' => now(),
@@ -186,6 +201,32 @@ protected function fetchComposerJson(string $owner, string $repo, ?string $token
186201
return null;
187202
}
188203

204+
protected function fetchNativephpJson(string $owner, string $repo, ?string $token): ?array
205+
{
206+
$request = Http::timeout(30);
207+
208+
if ($token) {
209+
$request = $request->withToken($token);
210+
}
211+
212+
$response = $request->get("https://api.github.com/repos/{$owner}/{$repo}/contents/nativephp.json");
213+
214+
if ($response->failed()) {
215+
return null;
216+
}
217+
218+
$content = $response->json('content');
219+
$encoding = $response->json('encoding');
220+
221+
if ($encoding === 'base64' && $content) {
222+
$decoded = base64_decode($content);
223+
224+
return json_decode($decoded, true);
225+
}
226+
227+
return null;
228+
}
229+
189230
protected function checkDirectoryHasFiles(array $tree, string $prefix): bool
190231
{
191232
foreach ($tree as $item) {

0 commit comments

Comments
 (0)