diff --git a/app/Filament/Pages/ResourceAuditLog.php b/app/Filament/Pages/ResourceAuditLog.php index 5bc89a75af..95bd3089d0 100644 --- a/app/Filament/Pages/ResourceAuditLog.php +++ b/app/Filament/Pages/ResourceAuditLog.php @@ -139,6 +139,7 @@ protected function getEventColor(string $event): string 'deleted' => 'danger', 'pivotAttached' => 'info', 'pivotDetached' => 'warning', + 'resetAllLeaderboardEntries' => 'danger', default => 'info', }; } diff --git a/app/Filament/Resources/AchievementResource.php b/app/Filament/Resources/AchievementResource.php index 31328e5592..639354882b 100644 --- a/app/Filament/Resources/AchievementResource.php +++ b/app/Filament/Resources/AchievementResource.php @@ -387,7 +387,6 @@ public static function getPages(): array { return [ 'index' => Pages\Index::route('/'), - 'create' => Pages\Create::route('/create'), 'view' => Pages\Details::route('/{record}'), 'edit' => Pages\Edit::route('/{record}/edit'), 'audit-log' => Pages\AuditLog::route('/{record}/audit-log'), diff --git a/app/Filament/Resources/AchievementResource/Pages/Create.php b/app/Filament/Resources/AchievementResource/Pages/Create.php deleted file mode 100644 index 3390bfb5a2..0000000000 --- a/app/Filament/Resources/AchievementResource/Pages/Create.php +++ /dev/null @@ -1,13 +0,0 @@ -user(); + + return $user->can('manage', Leaderboard::class); + } + + public function form(Form $form): Form + { + return $form + ->schema([ + + ]); + } + + public function table(Table $table): Table + { + /** @var User $user */ + $user = auth()->user(); + + return $table + ->recordTitleAttribute('title') + ->columns([ + Tables\Columns\TextColumn::make('ID') + ->label('ID') + ->searchable() + ->toggleable(), + + Tables\Columns\TextColumn::make('Title') + ->label('Title') + ->description(fn (Leaderboard $record): string => $record->description) + ->searchable(), + + Tables\Columns\TextColumn::make('Format') + ->label('Format') + ->formatStateUsing(fn (string $state) => ValueFormat::toString($state)) + ->toggleable(), + + Tables\Columns\TextColumn::make('entries_count') + ->label('Entries') + ->counts('entries') + ->numeric() + ->toggleable(), + + Tables\Columns\TextColumn::make('LowerIsBetter') + ->label('Lower Is Better') + ->toggleable(isToggledHiddenByDefault: true), + + Tables\Columns\TextColumn::make('DisplayOrder') + ->label('Display Order') + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->searchPlaceholder('Search (ID, Title)') + ->filters([ + + ]) + ->headerActions([ + + ]) + ->actions([ + Tables\Actions\ActionGroup::make([ + Action::make('view_entries') + ->label('View entries') + ->url(fn (Leaderboard $leaderboard) => route('filament.admin.resources.leaderboards.view', ['record' => $leaderboard])) + ->visible(function (Leaderboard $leaderboard) { + /** @var User $user */ + $user = auth()->user(); + + return $user->can('manage', $leaderboard) && !$user->can('update', $leaderboard); + }), + + Action::make('edit') + ->label('Edit') + ->icon('heroicon-s-pencil') + ->url(fn (Leaderboard $leaderboard) => route('filament.admin.resources.leaderboards.edit', ['record' => $leaderboard])) + ->visible(function (Leaderboard $leaderboard) { + /** @var User $user */ + $user = auth()->user(); + + return $user->can('update', $leaderboard); + }), + + Action::make('reset_all_entries') + ->label('Delete All Entries') + ->icon('heroicon-s-trash') + ->color('danger') + ->requiresConfirmation() + ->modalDescription("Are you sure you want to permanently delete all entries of this leaderboard?") + ->action(function (Leaderboard $leaderboard) { + /** @var User $user */ + $user = auth()->user(); + + if (!$user->can('resetAllEntries', $leaderboard)) { + return; + } + + $leaderboard->entries()->delete(); + + activity() + ->useLog('default') + ->causedBy($user) + ->performedOn($leaderboard) + ->event('resetAllLeaderboardEntries') + ->log('Reset All Leaderboard Entries'); + }) + ->visible(function (Leaderboard $leaderboard) { + /** @var User $user */ + $user = auth()->user(); + + return $user->can('resetAllEntries', $leaderboard); + }), + + Action::make('delete_leaderboard') + ->label('Delete Leaderboard') + ->icon('heroicon-s-trash') + ->color('danger') + ->requiresConfirmation() + ->modalDescription("Are you sure you want to permanently delete this leaderboard?") + ->action(function (Leaderboard $leaderboard) { + /** @var User $user */ + $user = auth()->user(); + + // TODO use soft deletes + if (!$user->can('forceDelete', $leaderboard)) { + return; + } + + $leaderboard->forceDelete(); + }) + ->visible(function (Leaderboard $leaderboard) { + /** @var User $user */ + $user = auth()->user(); + + return $user->can('forceDelete', $leaderboard); + }), + ]), + ]) + ->bulkActions([ + + ]) + ->paginated([25, 50, 100]) + ->defaultSort(function (Builder $query): Builder { + return $query + ->orderBy('DisplayOrder') + ->orderBy('Created', 'asc'); + }) + ->reorderRecordsTriggerAction( + fn (Action $action, bool $isReordering) => $action + ->button() + ->label($isReordering ? 'Stop reordering' : 'Start reordering'), + ) + ->reorderable('DisplayOrder', $this->canReorderLeaderboards()) + ->checkIfRecordIsSelectableUsing( + fn (Model $record): bool => $user->can('update', $record->loadMissing('game')), + ); + } + + public function reorderTable(array $order): void + { + parent::reorderTable($order); + + /** @var User $user */ + $user = auth()->user(); + /** @var Game $game */ + $game = $this->getOwnerRecord(); + + // We don't want to flood the logs with reordering activity. + // We'll throttle these events by 10 minutes. + $recentReorderingActivity = DB::table('audit_log') + ->where('causer_id', $user->id) + ->where('subject_id', $game->id) + ->where('subject_type', 'game') + ->where('event', 'reorderedLeaderboards') + ->where('created_at', '>=', now()->subMinutes(10)) + ->first(); + + // If the user didn't recently reorder leaderboards, write a new log. + if (!$recentReorderingActivity) { + activity() + ->useLog('default') + ->causedBy(auth()->user()) + ->performedOn($game) + ->event('reorderedLeaderboards') + ->log('Reordered Leaderboards'); + } + } + + private function canReorderLeaderboards(): bool + { + /** @var User $user */ + $user = auth()->user(); + + /** @var Leaderboard $game */ + $game = $this->getOwnerRecord(); + + return $user->can('update', $game); + } +} diff --git a/app/Filament/Resources/LeaderboardResource.php b/app/Filament/Resources/LeaderboardResource.php index 00e98a4aed..a27caee9e7 100644 --- a/app/Filament/Resources/LeaderboardResource.php +++ b/app/Filament/Resources/LeaderboardResource.php @@ -15,6 +15,7 @@ use Filament\Forms\Form; use Filament\Infolists; use Filament\Infolists\Infolist; +use Filament\Pages\Page; use Filament\Tables; use Filament\Tables\Table; use Illuminate\Contracts\Support\Htmlable; @@ -63,9 +64,29 @@ public static function infolist(Infolist $infolist): Infolist Infolists\Components\Section::make('Metadata') ->columns(['md' => 2, 'xl' => 3, '2xl' => 4]) ->schema([ + Infolists\Components\TextEntry::make('game.title') + ->url(function (Leaderboard $record) { + if (request()->user()->can('manage', Game::class)) { + return GameResource::getUrl('view', ['record' => $record->game->id]); + } + + return null; + }) + ->extraAttributes(function (): array { + if (request()->user()->can('manage', Game::class)) { + return ['class' => 'underline']; + } + + return []; + }), + Infolists\Components\TextEntry::make('Title'), - Infolists\Components\TextEntry::make('game.title'), + Infolists\Components\TextEntry::make('Description'), + + Infolists\Components\TextEntry::make('LowerIsBetter') + ->label('Lower Is Better') + ->formatStateUsing(fn (string $state): string => $state === '1' ? 'Yes' : 'No'), ]), ]); } @@ -74,7 +95,31 @@ public static function form(Form $form): Form { return $form ->schema([ - + Forms\Components\Section::make('Primary Details') + ->icon('heroicon-m-key') + ->columns(['md' => 2, 'xl' => 3, '2xl' => 4]) + ->schema([ + Forms\Components\TextInput::make('Title') + ->required() + ->minLength(2) + ->maxLength(255), + + Forms\Components\TextInput::make('Description') + ->maxLength(255), + + Forms\Components\Select::make('Format') + ->options( + collect(ValueFormat::cases()) + ->mapWithKeys(fn ($format) => [$format => ValueFormat::toString($format)]) + ->toArray() + ) + ->required(), + + Forms\Components\Toggle::make('LowerIsBetter') + ->label('Lower Is Better') + ->inline(false) + ->helperText('Useful for speedrun leaderboards and similar scenarios.'), + ]), ]); } @@ -179,12 +224,21 @@ public static function getRelations(): array ]; } + public static function getRecordSubNavigation(Page $page): array + { + return $page->generateNavigationitems([ + Pages\Details::class, + Pages\AuditLog::class, + ]); + } + public static function getPages(): array { return [ 'index' => Pages\Index::route('/'), 'view' => Pages\Details::route('/{record}'), 'edit' => Pages\Edit::route('/{record}/edit'), + 'audit-log' => Pages\AuditLog::route('/{record}/audit-log'), ]; } diff --git a/app/Filament/Resources/LeaderboardResource/Pages/AuditLog.php b/app/Filament/Resources/LeaderboardResource/Pages/AuditLog.php new file mode 100644 index 0000000000..f552359d9e --- /dev/null +++ b/app/Filament/Resources/LeaderboardResource/Pages/AuditLog.php @@ -0,0 +1,11 @@ +logOnly([ + 'Title', + 'Description', + 'Format', + 'LowerIsBetter', + ]) + ->logOnlyDirty() + ->dontSubmitEmptyLogs(); + } + // == search public function toSearchableArray(): array diff --git a/app/Policies/LeaderboardPolicy.php b/app/Policies/LeaderboardPolicy.php index b5f89b65cd..9b47f5e926 100644 --- a/app/Policies/LeaderboardPolicy.php +++ b/app/Policies/LeaderboardPolicy.php @@ -4,6 +4,7 @@ namespace App\Policies; +use App\Models\Game; use App\Models\Leaderboard; use App\Models\Role; use App\Models\User; @@ -32,23 +33,36 @@ public function view(?User $user, Leaderboard $leaderboard): bool return true; } - public function create(User $user): bool + public function create(User $user, ?Game $game = null): bool { - // TODO all full devs. jr devs if they have a claim + if ($game && $user->hasRole(Role::DEVELOPER_JUNIOR)) { + return $user->hasActiveClaimOnGameId($game->id); + } - return false; + return $user->hasAnyRole([ + Role::DEVELOPER_STAFF, + Role::DEVELOPER, + ]); } public function update(User $user, Leaderboard $leaderboard): bool { - // TODO all full devs. jr devs if they have a claim + if ($user->hasRole(Role::DEVELOPER_JUNIOR)) { + return $user->is($leaderboard->developer); + } - return false; + return $user->hasAnyRole([ + Role::DEVELOPER_STAFF, + Role::DEVELOPER, + ]); } public function delete(User $user, Leaderboard $leaderboard): bool { - return false; + return $user->hasAnyRole([ + Role::DEVELOPER_STAFF, + Role::DEVELOPER, + ]); } public function restore(User $user, Leaderboard $leaderboard): bool @@ -58,6 +72,17 @@ public function restore(User $user, Leaderboard $leaderboard): bool public function forceDelete(User $user, Leaderboard $leaderboard): bool { - return false; + return $user->hasAnyRole([ + Role::DEVELOPER_STAFF, + Role::DEVELOPER, + ]); + } + + public function resetAllEntries(User $user, Leaderboard $leaderboard): bool + { + return $user->hasAnyRole([ + Role::DEVELOPER_STAFF, + Role::DEVELOPER, + ]); } } diff --git a/lang/en/filament.php b/lang/en/filament.php index 5c14b2669c..5ab3f430f3 100755 --- a/lang/en/filament.php +++ b/lang/en/filament.php @@ -14,6 +14,8 @@ 'pivotAttached' => 'Attached', 'pivotDetached' => 'Detached', 'reorderedAchievements' => 'Reordered achievements', + 'reorderedLeaderboards' => 'Reordered leaderboards', + 'resetAllLeaderboardEntries' => 'Reset all leaderboard entries', ], ], ];