Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/Filament/Pages/ResourceAuditLog.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ protected function getEventColor(string $event): string
return match ($event) {
'approvedScreenshot' => 'success',
'changedScreenshotType' => 'info',
'clearedScreenshots' => 'danger',
'created' => 'success',
'creditCreated' => 'success',
'creditDeleted' => 'danger',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use App\Models\GameScreenshot;
use App\Models\User;
use App\Platform\Actions\AddGameScreenshotAction;
use App\Platform\Actions\ClearGameScreenshotsFromGamePageAction;
use App\Platform\Enums\GameScreenshotStatus;
use App\Platform\Enums\ScreenshotType;
use App\Rules\DisallowAnimatedImageRule;
Expand All @@ -16,6 +17,7 @@
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Actions\RestoreAction;
use Filament\Forms;
use Filament\Notifications\Notification;
use Filament\Resources\RelationManagers\RelationManager;
Expand Down Expand Up @@ -64,6 +66,9 @@ public function table(Table $table): Table
/** @var Game $game */
$game = $this->getOwnerRecord();

/** @var User $user */
$user = Auth::user();

return $table
->headerActions([
Action::make('upload_screenshot')
Expand Down Expand Up @@ -139,14 +144,42 @@ public function table(Table $table): Table
->title("{$label} uploaded successfully")
->send();
}),

ActionGroup::make([
Action::make('clear_screenshots_from_game_page')
->label('Clear Screenshots')
->icon('heroicon-o-trash')
->color('danger')
->requiresConfirmation()
->modalHeading("Clear this game's screenshots?")
->modalDescription('All screenshots will be moved to the trash and the game will use placeholder images. You can restore them later.')
->modalSubmitActionLabel('Clear Screenshots')
->action(function () use ($game): void {
$clearedCount = (new ClearGameScreenshotsFromGamePageAction())->execute($game);

$this->logScreenshotActivity($game)
->event('clearedScreenshots')
->log("Cleared {$clearedCount} screenshot(s)");

Notification::make()
->success()
->title('Screenshots cleared')
->send();
}),
])
->label('More')
->icon('heroicon-m-ellipsis-vertical')
->iconButton()
->tooltip('More actions')
->visible(fn (): bool => $user->can('clearScreenshots', $game) && $game->gameScreenshots()->exists()),
])
->modifyQueryUsing(function (Builder $query) {
/** @var Builder<GameScreenshot> $query */
$query->with(['media', 'game.system'])
->orderByType()
->orderBy('order_column');

if (!$this->shouldShowArchivedScreenshots()) {
if (!$this->shouldShowRejectedOrReplacedScreenshots()) {
$query->whereNotIn('status', [
GameScreenshotStatus::Rejected->value,
GameScreenshotStatus::Replaced->value,
Expand Down Expand Up @@ -212,6 +245,11 @@ public function table(Table $table): Table
)
->color(fn (GameScreenshot $record): ?string => $record->has_wrong_resolution ? 'danger' : null)
->icon(fn (GameScreenshot $record): ?string => $record->has_wrong_resolution ? 'heroicon-o-exclamation-triangle' : null),

Tables\Columns\TextColumn::make('deleted_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
Tables\Filters\SelectFilter::make('type')
Expand All @@ -223,6 +261,11 @@ public function table(Table $table): Table
->placeholder('Published + Pending')
->options(fn (): array => $this->getStatusFilterOptions()),

Tables\Filters\TrashedFilter::make()
->label('Deleted screenshots')
->placeholder('Current screenshots')
->trueLabel('All screenshots')
->falseLabel('Deleted screenshots'),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This filter feels wrong.

Image

I'm not viewing "Currently Deleted screenshots", I'm viewing non-deleted screenshots.

And I'm not sure that I like the way it combines with the other filter:

Image

I feel like Deleted should just be its own Status. What does it mean to have a Deleted Published screenshot?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrestled with this too when figuring out what solution I wanted to build for this functionality. My original idea was to have an "Archived" status, but that felt awfully ambiguous with "Rejected" also in the mix.

A deleted screenshot having a status of published means, on restore, the screenshot will be published again. That makes cleanly undoing the delete possible.

Here's where I've landed in latest - no more trashed entity filter. It's disguised as a Status to hopefully be more intuitive even though the underlying model is unchanged:
Screenshot 2026-05-06 at 7 44 35 PM

])
->emptyStateHeading('No screenshots yet')
->emptyStateDescription('Upload screenshots using the button above.')
Expand All @@ -234,7 +277,7 @@ public function table(Table $table): Table
->iconButton()
->tooltip('Set as Primary')
->requiresConfirmation()
->hidden(fn (GameScreenshot $record): bool => $record->is_primary)
->hidden(fn (GameScreenshot $record): bool => $record->is_primary || $record->trashed())
->action(function (GameScreenshot $record) use ($game): void {
DB::transaction(function () use ($record) {
// Demote the current primary of the same type via Eloquent
Expand Down Expand Up @@ -269,7 +312,7 @@ public function table(Table $table): Table
->label('Approve')
->icon('heroicon-o-check-circle')
->color('success')
->visible(fn (GameScreenshot $record): bool => $record->status !== GameScreenshotStatus::Approved)
->visible(fn (GameScreenshot $record): bool => !$record->trashed() && $record->status !== GameScreenshotStatus::Approved)
->action(function (GameScreenshot $record) use ($game): void {
$oldStatus = $record->status;

Expand All @@ -288,6 +331,7 @@ public function table(Table $table): Table
Action::make('change_type')
->label('Change Type')
->icon('heroicon-o-tag')
->visible(fn (GameScreenshot $record): bool => !$record->trashed())
->schema([
Forms\Components\Select::make('type')
->label('Type')
Expand Down Expand Up @@ -320,7 +364,7 @@ public function table(Table $table): Table
->icon('heroicon-o-x-circle')
->color('danger')
->requiresConfirmation()
->visible(fn (GameScreenshot $record): bool => !$record->is_primary && $record->status !== GameScreenshotStatus::Rejected)
->visible(fn (GameScreenshot $record): bool => !$record->trashed() && !$record->is_primary && $record->status !== GameScreenshotStatus::Rejected)
->action(function (GameScreenshot $record) use ($game): void {
$oldStatus = $record->status;

Expand All @@ -344,14 +388,15 @@ public function table(Table $table): Table
->requiresConfirmation()
->modalDescription(fn (GameScreenshot $record): string => $record->is_primary
? 'This is a primary screenshot. The next published screenshot of this type will be promoted automatically, or the placeholder will be restored.'
: 'Are you sure you want to delete this screenshot?')
: 'Are you sure you want to permanently delete this screenshot?')
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do I get this button? I can't delete individual screenshots.

Image

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the previous diff, it authorized implicitly against the deletion policy (original uploader + still Pending). I think we should remove it altogether TBH, which I've done in latest.

->using(function (GameScreenshot $record) use ($game): void {
$screenshotUrl = $record->media?->getUrl();
$media = $record->media;
$type = $record->type->label();
$wasPrimary = $record->is_primary;

$record->media?->delete();
$record->delete();
$record->forceDelete();
$media?->delete();

$this->logScreenshotActivity($game)
->withProperty('attributes', [
Expand All @@ -362,6 +407,8 @@ public function table(Table $table): Table
->event('deletedScreenshot')
->log('Deleted screenshot');
}),

RestoreAction::make(),
]),
]);
}
Expand All @@ -382,8 +429,11 @@ private function getStatusFilterOptions(): array
$game = $this->getOwnerRecord();

$selectedType = data_get($this->getTableFilterState('type'), 'value');
$selectedTrashed = $this->getSelectedTrashedFilter();

$counts = $game->gameScreenshots()
->when($selectedTrashed === true, fn (Builder $query) => $query->withTrashed())
->when($selectedTrashed === false, fn (Builder $query) => $query->onlyTrashed())
->when($selectedType, fn (Builder $query) => $query->where('type', $selectedType))
->whereIn('status', [
GameScreenshotStatus::Approved->value,
Expand Down Expand Up @@ -475,13 +525,29 @@ private function getScreenshotHelperText(): ?string
return $text;
}

private function shouldShowArchivedScreenshots(): bool
private function shouldShowRejectedOrReplacedScreenshots(): bool
{
if ($this->getSelectedTrashedFilter() === false) {
return true;
}

$selectedStatus = data_get($this->getTableFilterState('status'), 'value');

return in_array($selectedStatus, [
GameScreenshotStatus::Rejected->value,
GameScreenshotStatus::Replaced->value,
], true);
}

/**
* Normalize the trashed filter state to a tri-state value.
* Filament serializes booleans as '1'/'0' through the form layer,
* so === true / === false comparisons against the raw value silently fail.
*/
private function getSelectedTrashedFilter(): ?bool
{
$value = data_get($this->getTableFilterState('trashed'), 'value');

return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
}
}
2 changes: 2 additions & 0 deletions app/Models/GameScreenshot.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\EloquentSortable\SortableTrait;
use Spatie\MediaLibrary\MediaCollections\Models\Media;

class GameScreenshot extends BaseModel
{
/** @use HasFactory<GameScreenshotFactory> */
use HasFactory;
use SoftDeletes;
use SortableTrait;

protected $table = 'game_screenshots';
Expand Down
36 changes: 36 additions & 0 deletions app/Platform/Actions/ClearGameScreenshotsFromGamePageAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace App\Platform\Actions;

use App\Models\Game;
use App\Models\GameScreenshot;
use App\Platform\Enums\ScreenshotType;
use Illuminate\Support\Facades\DB;

class ClearGameScreenshotsFromGamePageAction
{
public function execute(Game $game): int
{
return DB::transaction(function () use ($game): int {
$screenshotIds = $game->gameScreenshots()
->pluck('id');

if ($screenshotIds->isEmpty()) {
return 0;
}

GameScreenshot::whereKey($screenshotIds)
->update(['is_primary' => false]);

$deletedCount = GameScreenshot::whereKey($screenshotIds)
->delete();

$game->syncLegacyScreenshotFields(ScreenshotType::Title);
$game->syncLegacyScreenshotFields(ScreenshotType::Ingame);

return $deletedCount;
});
}
}
3 changes: 3 additions & 0 deletions app/Platform/Services/GameScreenshotValidationService.php
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A screenshot cannot be re-uploaded if it's identical to a deleted one.

Image

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. This is intentional, especially through the player-facing UI. Reasoning: if some back office action deleted the image, it shouldn't be reuploaded by a player again for back office review.

Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ public function validateHash(UploadedFile $file, Game $game): string
$hash = sha1_file($file->getRealPath());

// Reject duplicates based on SHA1 within this game's non-rejected screenshots.
// Trashed screenshots are included so that re-uploading a cleared screenshot
// can't bypass the check and create a collision when the original is restored.
$isDuplicate = $game->gameScreenshots()
->withTrashed()
->where('status', '!=', GameScreenshotStatus::Rejected)
->whereHas('media', function ($query) use ($hash) {
$query->where('custom_properties->sha1', $hash);
Expand Down
9 changes: 9 additions & 0 deletions app/Policies/GamePolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,15 @@ public function updateField(User $user, Game $game, string $fieldName): bool
return in_array($fieldName, $allowedFieldsForUser, true);
}

public function clearScreenshots(User $user, Game $game): bool
{
return $user->hasAnyRole([
Role::ADMINISTRATOR,
Role::GAME_EDITOR,
Role::MEDIA_EDITOR,
]);
}

public static function canDeveloperJuniorUpdateGame(User $user, Game $game): bool
{
// If the user has a DEVELOPER_JUNIOR role, they need to have a claim
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
public function up(): void
{
Schema::table('game_screenshots', function (Blueprint $table) {
$table->softDeletes()->after('updated_at');
});
}

public function down(): void
{
Schema::table('game_screenshots', function (Blueprint $table) {
$table->dropSoftDeletes();
});
}
};
1 change: 1 addition & 0 deletions lang/en/filament.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
'achievementsAssignedToGroup' => 'Achievements assigned to group',
'approvedScreenshot' => 'Approved screenshot',
'changedScreenshotType' => 'Changed screenshot type',
'clearedScreenshots' => 'Cleared screenshots',
'created' => 'Created',
'creditCreated' => 'Credit added',
'creditDeleted' => 'Credit removed',
Expand Down
Loading
Loading