From ea5683d080db8ce06c00a980bfcbfdbe4e190645 Mon Sep 17 00:00:00 2001 From: Kyrch Date: Thu, 27 Jun 2024 06:22:16 -0300 Subject: [PATCH] feat: action logs (#702) --- app/Actions/Discord/DiscordThreadAction.php | 45 +- .../Filament/Actions/HasActionLogs.php | 120 ++++++ .../Filament/Actions/HasPivotActionLogs.php | 45 ++ app/Enums/Actions/ActionLogStatus.php | 33 ++ app/Filament/Actions/Base/AttachAction.php | 81 ++++ app/Filament/Actions/Base/CreateAction.php | 33 ++ app/Filament/Actions/Base/DeleteAction.php | 30 ++ app/Filament/Actions/Base/DetachAction.php | 35 ++ app/Filament/Actions/Base/EditAction.php | 35 ++ .../Actions/Base/ForceDeleteAction.php | 27 ++ app/Filament/Actions/Base/RestoreAction.php | 30 ++ app/Filament/Actions/Base/ViewAction.php | 25 ++ app/Filament/Actions/BaseAction.php | 7 + .../Actions/Discord/DiscordThreadAction.php | 11 +- .../Actions/Models/AssignHashidsAction.php | 2 +- .../Models/Wiki/Anime/BackfillAnimeAction.php | 4 +- .../Wiki/Studio/BackfillStudioAction.php | 7 +- .../Models/Wiki/Video/BackfillAudioAction.php | 11 +- .../Actions/Storage/StorageAction.php | 4 + .../BulkActions/Base/DeleteBulkAction.php | 37 ++ .../BulkActions/Base/DetachBulkAction.php | 37 ++ .../Base/ForceDeleteBulkAction.php | 32 ++ .../BulkActions/Base/RestoreBulkAction.php | 37 ++ app/Filament/BulkActions/BaseBulkAction.php | 12 + .../Components/Columns/TextColumn.php | 3 +- .../Components/Infolist/TextEntry.php | 3 +- .../HeaderActions/Base/CreateHeaderAction.php | 23 + .../HeaderActions/Base/DeleteHeaderAction.php | 30 ++ .../HeaderActions/Base/EditHeaderAction.php | 25 ++ .../Base/ForceDeleteHeaderAction.php | 27 ++ .../Base/RestoreHeaderAction.php | 30 ++ .../HeaderActions/Base/ViewHeaderAction.php | 28 ++ .../HeaderActions/BaseHeaderAction.php | 7 + .../Discord/DiscordThreadHeaderAction.php | 13 +- .../Models/AssignHashidsHeaderAction.php | 2 +- .../Wiki/Anime/BackfillAnimeHeaderAction.php | 6 +- .../Studio/BackfillStudioHeaderAction.php | 7 +- .../Wiki/Video/BackfillAudioHeaderAction.php | 2 +- .../Storage/StorageHeaderAction.php | 4 + .../Base/ActionLogRelationManager.php | 67 +++ .../RelationManagers/BaseRelationManager.php | 72 +--- .../Wiki/ImageRelationManager.php | 10 + .../Wiki/Video/ScriptRelationManager.php | 10 + .../Wiki/VideoRelationManager.php | 10 + app/Filament/Resources/Admin/ActionLog.php | 394 +++++++++++++++++ .../Admin/ActionLog/Pages/ListActionLogs.php | 31 ++ .../Admin/ActionLog/Pages/ViewActionLog.php | 31 ++ app/Filament/Resources/Admin/Announcement.php | 10 +- app/Filament/Resources/Admin/Dump.php | 10 +- app/Filament/Resources/Admin/Feature.php | 10 +- .../Resources/Admin/FeaturedTheme.php | 10 +- app/Filament/Resources/Auth/Permission.php | 12 +- app/Filament/Resources/Auth/Role.php | 12 +- app/Filament/Resources/Auth/User.php | 15 +- .../Resources/Base/BaseCreateResource.php | 11 +- .../Resources/Base/BaseEditResource.php | 32 +- .../Resources/Base/BaseListResources.php | 4 +- .../Resources/Base/BaseViewResource.php | 18 +- app/Filament/Resources/BaseResource.php | 68 ++- .../Resources/Discord/DiscordThread.php | 10 +- app/Filament/Resources/Document/Page.php | 10 +- app/Filament/Resources/List/Playlist.php | 13 +- .../Resources/List/Playlist/Track.php | 10 +- app/Filament/Resources/Wiki/Anime.php | 21 +- app/Filament/Resources/Wiki/Anime/Synonym.php | 10 +- app/Filament/Resources/Wiki/Anime/Theme.php | 13 +- .../Resources/Wiki/Anime/Theme/Entry.php | 11 +- app/Filament/Resources/Wiki/Artist.php | 19 +- app/Filament/Resources/Wiki/Audio.php | 23 +- .../Wiki/Audio/Pages/CreateAudio.php | 16 - .../Resources/Wiki/ExternalResource.php | 17 +- app/Filament/Resources/Wiki/Group.php | 11 +- app/Filament/Resources/Wiki/Image.php | 39 +- .../Wiki/Image/Pages/CreateImage.php | 16 - app/Filament/Resources/Wiki/Series.php | 11 +- app/Filament/Resources/Wiki/Song.php | 15 +- app/Filament/Resources/Wiki/Studio.php | 15 +- app/Filament/Resources/Wiki/Video.php | 27 +- .../Wiki/Video/Pages/CreateVideo.php | 16 - app/Filament/Resources/Wiki/Video/Script.php | 22 +- .../Wiki/Video/Script/Pages/CreateScript.php | 16 - app/Filament/TableActions/BaseTableAction.php | 16 + .../Discord/DiscordEditMessageTableAction.php | 3 + .../Discord/DiscordSendMessageTableAction.php | 3 + .../Storage/StorageTableAction.php | 23 +- app/Models/Admin/ActionLog.php | 408 ++++++++++++++++++ app/Models/Auth/User.php | 11 + app/Models/BaseModel.php | 12 + ..._06_25_145723_create_action_logs_table.php | 48 +++ lang/en/enums.php | 6 + lang/en/filament.php | 20 + phpstan.neon | 3 + 92 files changed, 2373 insertions(+), 328 deletions(-) create mode 100644 app/Concerns/Filament/Actions/HasActionLogs.php create mode 100644 app/Concerns/Filament/Actions/HasPivotActionLogs.php create mode 100644 app/Enums/Actions/ActionLogStatus.php create mode 100644 app/Filament/Actions/Base/AttachAction.php create mode 100644 app/Filament/Actions/Base/CreateAction.php create mode 100644 app/Filament/Actions/Base/DeleteAction.php create mode 100644 app/Filament/Actions/Base/DetachAction.php create mode 100644 app/Filament/Actions/Base/EditAction.php create mode 100644 app/Filament/Actions/Base/ForceDeleteAction.php create mode 100644 app/Filament/Actions/Base/RestoreAction.php create mode 100644 app/Filament/Actions/Base/ViewAction.php create mode 100644 app/Filament/BulkActions/Base/DeleteBulkAction.php create mode 100644 app/Filament/BulkActions/Base/DetachBulkAction.php create mode 100644 app/Filament/BulkActions/Base/ForceDeleteBulkAction.php create mode 100644 app/Filament/BulkActions/Base/RestoreBulkAction.php create mode 100644 app/Filament/HeaderActions/Base/CreateHeaderAction.php create mode 100644 app/Filament/HeaderActions/Base/DeleteHeaderAction.php create mode 100644 app/Filament/HeaderActions/Base/EditHeaderAction.php create mode 100644 app/Filament/HeaderActions/Base/ForceDeleteHeaderAction.php create mode 100644 app/Filament/HeaderActions/Base/RestoreHeaderAction.php create mode 100644 app/Filament/HeaderActions/Base/ViewHeaderAction.php create mode 100644 app/Filament/RelationManagers/Base/ActionLogRelationManager.php create mode 100644 app/Filament/Resources/Admin/ActionLog.php create mode 100644 app/Filament/Resources/Admin/ActionLog/Pages/ListActionLogs.php create mode 100644 app/Filament/Resources/Admin/ActionLog/Pages/ViewActionLog.php delete mode 100644 app/Filament/Resources/Wiki/Audio/Pages/CreateAudio.php delete mode 100644 app/Filament/Resources/Wiki/Image/Pages/CreateImage.php delete mode 100644 app/Filament/Resources/Wiki/Video/Pages/CreateVideo.php delete mode 100644 app/Filament/Resources/Wiki/Video/Script/Pages/CreateScript.php create mode 100644 app/Models/Admin/ActionLog.php create mode 100644 database/migrations/2024_06_25_145723_create_action_logs_table.php diff --git a/app/Actions/Discord/DiscordThreadAction.php b/app/Actions/Discord/DiscordThreadAction.php index 6a3ce4e9b..733cdefa3 100644 --- a/app/Actions/Discord/DiscordThreadAction.php +++ b/app/Actions/Discord/DiscordThreadAction.php @@ -6,6 +6,7 @@ namespace App\Actions\Discord; use App\Models\Discord\DiscordThread; use App\Models\Wiki\Anime; +use Exception; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Http; @@ -21,32 +22,38 @@ class DiscordThreadAction * * @param Anime $anime * @param array $fields - * - * @return void + * @return Exception|null */ - public function handle(Anime $anime, array $fields): void + public function handle(Anime $anime, array $fields): ?Exception { - $anime->load(Anime::RELATION_IMAGES); + try { + $anime->load(Anime::RELATION_IMAGES); - $anime->name = Arr::get($fields, 'name'); + $anime->name = Arr::get($fields, 'name'); - /** @var \Illuminate\Filesystem\FilesystemAdapter */ - $fs = Storage::disk(Config::get('image.disk')); + /** @var \Illuminate\Filesystem\FilesystemAdapter */ + $fs = Storage::disk(Config::get('image.disk')); - $anime->images->each(fn ($image) => Arr::set($image, 'link', $fs->url($image->path))); + $anime->images->each(fn ($image) => Arr::set($image, 'link', $fs->url($image->path))); - $response = Http::withHeaders(['x-api-key' => Config::get('services.discord.api_key')]) - ->acceptJson() - ->post(Config::get('services.discord.api_url') . '/thread', $anime->toArray()) - ->throw() - ->json(); + $response = Http::withHeaders(['x-api-key' => Config::get('services.discord.api_key')]) + ->acceptJson() + ->post(Config::get('services.discord.api_url') . '/thread', $anime->toArray()) + ->throw() + ->json(); - if (Arr::has($response, 'id')) { - DiscordThread::query()->create([ - DiscordThread::ATTRIBUTE_NAME => Arr::get($response, 'name'), - DiscordThread::ATTRIBUTE_ID => intval(Arr::get($response, 'id')), - DiscordThread::ATTRIBUTE_ANIME => $anime->getKey(), - ]); + if (Arr::has($response, 'id')) { + DiscordThread::query()->create([ + DiscordThread::ATTRIBUTE_NAME => Arr::get($response, 'name'), + DiscordThread::ATTRIBUTE_ID => intval(Arr::get($response, 'id')), + DiscordThread::ATTRIBUTE_ANIME => $anime->getKey(), + ]); + } + + return null; + + } catch (Exception $e) { + return $e; } } diff --git a/app/Concerns/Filament/Actions/HasActionLogs.php b/app/Concerns/Filament/Actions/HasActionLogs.php new file mode 100644 index 000000000..428d11960 --- /dev/null +++ b/app/Concerns/Filament/Actions/HasActionLogs.php @@ -0,0 +1,120 @@ +batchId = Str::orderedUuid()->__toString(); + + return $this->batchId; + } + + /** + * Create an action log. + * + * @param mixed $action + * @param Model|null $record + * @param bool $shouldCreateNewBatchId + * @return void + */ + public function createActionLog(mixed $action, ?Model $record = null, ?bool $shouldCreateNewBatchId = true): void + { + if ($shouldCreateNewBatchId) { + $this->createBatchId(); + } + + // $record must be specified if in context of bulk action + $this->recordLog = $record ?? $this->getRecord(); + + $actionLog = ActionLog::modelActioned( + $this->batchId, + $action, + $this->recordLog, + ); + + $this->actionLog = $actionLog; + } + + /** + * Update the log for pivot actions. + * + * @param Model $relatedModel + * @param Model $pivot + * @return void + */ + public function updateLog(Model $relatedModel, Model $pivot): void + { + $this->actionLog->update([ + ActionLog::ATTRIBUTE_TARGET_TYPE => $relatedModel->getMorphClass(), + ActionLog::ATTRIBUTE_TARGET_ID => $relatedModel->getKey(), + ActionLog::ATTRIBUTE_MODEL_TYPE => $pivot->getMorphClass(), + ActionLog::ATTRIBUTE_MODEL_ID => $pivot->getKey(), + ]); + } + + /** + * Mark the action as failed. + * + * @param Throwable|string|null $exception + * @return void + */ + public function failedLog(Throwable|string|null $exception): void + { + $this->actionLog->failed($exception); + } + + /** + * Mark the action as finished if not failed. + * + * @return void + */ + public function finishedLog(): void + { + if (!$this->isFailedLog()) { + $this->actionLog->finished(); + } + } + + /** + * Mark batch action as finished where not failed. + * + * @return void + */ + public function batchFinishedLog(): void + { + $this->actionLog->batchFinished(); + } + + /** + * Check if the action is failed. + * + * @return bool + */ + public function isFailedLog(): bool + { + return $this->actionLog->isFailed(); + } +} diff --git a/app/Concerns/Filament/Actions/HasPivotActionLogs.php b/app/Concerns/Filament/Actions/HasPivotActionLogs.php new file mode 100644 index 000000000..821bf8625 --- /dev/null +++ b/app/Concerns/Filament/Actions/HasPivotActionLogs.php @@ -0,0 +1,45 @@ +getOwnerRecord(); + + /** @var BelongsToMany */ + $relation = $livewire->getRelationship(); + $pivotClass = $relation->getPivotClass(); + + $pivot = $pivotClass::query() + ->where($ownerRecord->getKeyName(), $ownerRecord->getKey()) + ->where($record->getKeyName(), $record->getKey()) + ->first(); + + ActionLog::modelPivot( + $actionName, + $livewire->getOwnerRecord(), + $record, + $pivot, + ); + } +} diff --git a/app/Enums/Actions/ActionLogStatus.php b/app/Enums/Actions/ActionLogStatus.php new file mode 100644 index 000000000..c409dc19e --- /dev/null +++ b/app/Enums/Actions/ActionLogStatus.php @@ -0,0 +1,33 @@ + 'primary', + ActionLogStatus::FAILED => 'danger', + ActionLogStatus::FINISHED => 'success', + }; + } +} diff --git a/app/Filament/Actions/Base/AttachAction.php b/app/Filament/Actions/Base/AttachAction.php new file mode 100644 index 000000000..1e484a2c1 --- /dev/null +++ b/app/Filament/Actions/Base/AttachAction.php @@ -0,0 +1,81 @@ +authorize('create'); + + $this->hidden(fn (BaseRelationManager $livewire) => !($livewire->getRelationship() instanceof BelongsToMany)); + + $this->recordSelect(function (BaseRelationManager $livewire) { + /** @var string */ + $model = $livewire->getTable()->getModel(); + $title = $livewire->getTable()->getRecordTitle(new $model); + return Select::make('recordId') + ->label($title) + ->useScout($model); + }); + + $this->form(function (Form $form, AttachAction $action) { + return $form + ->schema([ + $action->getRecordSelect(), + + TextInput::make(AnimeResource::ATTRIBUTE_AS) + ->label(__('filament.fields.anime.resources.as.name')) + ->helperText(__('filament.fields.anime.resources.as.help')) + ->visibleOn([ + AnimeResourceRelationManager::class, + ArtistResourceRelationManager::class, + SongResourceRelationManager::class, + StudioResourceRelationManager::class, + ResourceRelationManager::class, + ]), + + TextInput::make(ArtistSong::ATTRIBUTE_AS) + ->label(__('filament.fields.artist.songs.as.name')) + ->helperText(__('filament.fields.artist.songs.as.help')) + ->visibleOn([ + ArtistSongRelationManager::class, + SongArtistRelationManager::class, + ]), + ]); + }); + + $this->after(fn ($livewire, $record) => $this->pivotActionLog('Attach', $livewire, $record)); + } +} diff --git a/app/Filament/Actions/Base/CreateAction.php b/app/Filament/Actions/Base/CreateAction.php new file mode 100644 index 000000000..c6e96586b --- /dev/null +++ b/app/Filament/Actions/Base/CreateAction.php @@ -0,0 +1,33 @@ +after(function ($livewire, $record) { + if ($livewire instanceof BaseRelationManager) { + $this->pivotActionLog('Create and Attach', $livewire, $record); + } + }); + } +} diff --git a/app/Filament/Actions/Base/DeleteAction.php b/app/Filament/Actions/Base/DeleteAction.php new file mode 100644 index 000000000..232908387 --- /dev/null +++ b/app/Filament/Actions/Base/DeleteAction.php @@ -0,0 +1,30 @@ +label(__('filament.actions.base.delete')); + + $this->after(function ($record) { + ActionLog::modelDeleted($record); + }); + } +} diff --git a/app/Filament/Actions/Base/DetachAction.php b/app/Filament/Actions/Base/DetachAction.php new file mode 100644 index 000000000..e02b9e166 --- /dev/null +++ b/app/Filament/Actions/Base/DetachAction.php @@ -0,0 +1,35 @@ +label(__('filament.actions.base.detach')); + + $this->hidden(fn ($livewire) => !($livewire instanceof BaseRelationManager)); + + $this->authorize('delete'); + + $this->after(fn ($livewire, $record) => $this->pivotActionLog('Detach', $livewire, $record)); + } +} diff --git a/app/Filament/Actions/Base/EditAction.php b/app/Filament/Actions/Base/EditAction.php new file mode 100644 index 000000000..d43a53f7e --- /dev/null +++ b/app/Filament/Actions/Base/EditAction.php @@ -0,0 +1,35 @@ +label(__('filament.actions.base.edit')); + + $this->after(function ($livewire, $record) { + if ($livewire instanceof BaseRelationManager) { + $this->pivotActionLog('Update Attached', $livewire, $record); + } + }); + } +} diff --git a/app/Filament/Actions/Base/ForceDeleteAction.php b/app/Filament/Actions/Base/ForceDeleteAction.php new file mode 100644 index 000000000..09dc7540f --- /dev/null +++ b/app/Filament/Actions/Base/ForceDeleteAction.php @@ -0,0 +1,27 @@ +label(__('filament.actions.base.forcedelete')); + + $this->visible(true); + } +} diff --git a/app/Filament/Actions/Base/RestoreAction.php b/app/Filament/Actions/Base/RestoreAction.php new file mode 100644 index 000000000..b1a758f38 --- /dev/null +++ b/app/Filament/Actions/Base/RestoreAction.php @@ -0,0 +1,30 @@ +label(__('filament.actions.base.restore')); + + $this->after(function ($record) { + ActionLog::modelRestored($record); + }); + } +} diff --git a/app/Filament/Actions/Base/ViewAction.php b/app/Filament/Actions/Base/ViewAction.php new file mode 100644 index 000000000..64bbc7c22 --- /dev/null +++ b/app/Filament/Actions/Base/ViewAction.php @@ -0,0 +1,25 @@ +label(__('filament.actions.base.view')); + } +} diff --git a/app/Filament/Actions/BaseAction.php b/app/Filament/Actions/BaseAction.php index 26df1df5e..a48dce710 100644 --- a/app/Filament/Actions/BaseAction.php +++ b/app/Filament/Actions/BaseAction.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Filament\Actions; +use App\Concerns\Filament\Actions\HasActionLogs; use Filament\Support\Enums\MaxWidth; use Filament\Tables\Actions\Action; @@ -15,6 +16,8 @@ use Filament\Tables\Actions\Action; */ abstract class BaseAction extends Action { + use HasActionLogs; + /** * Initial setup for the action. * @@ -26,6 +29,10 @@ abstract class BaseAction extends Action $this->requiresConfirmation(); + $this->afterFormValidated(fn (BaseAction $action) => $this->createActionLog($action)); + + $this->after(fn () => $this->finishedLog()); + $this->modalWidth(MaxWidth::FourExtraLarge); } } \ No newline at end of file diff --git a/app/Filament/Actions/Discord/DiscordThreadAction.php b/app/Filament/Actions/Discord/DiscordThreadAction.php index 211bce379..93b374694 100644 --- a/app/Filament/Actions/Discord/DiscordThreadAction.php +++ b/app/Filament/Actions/Discord/DiscordThreadAction.php @@ -8,6 +8,7 @@ use App\Actions\Discord\DiscordThreadAction as DiscordThreadActionAction; use App\Filament\Actions\BaseAction; use App\Models\Discord\DiscordThread; use App\Models\Wiki\Anime; +use Exception; use Filament\Forms\Components\TextInput; use Filament\Forms\Form; @@ -32,9 +33,15 @@ class DiscordThreadAction extends BaseAction $this->fillForm(fn (Anime $record): array => ['name' => $record->getName()]); - $this->action(fn (Anime $record, array $data) => (new DiscordThreadActionAction())->handle($record, $data)); + $this->action(function (Anime $record, array $data) { + $action = (new DiscordThreadActionAction())->handle($record, $data); + + if ($action instanceof Exception) { + $this->failedLog($action); + } + }); } - + /** * Get the fields available on the action. * diff --git a/app/Filament/Actions/Models/AssignHashidsAction.php b/app/Filament/Actions/Models/AssignHashidsAction.php index 8d4f67462..029795ec7 100644 --- a/app/Filament/Actions/Models/AssignHashidsAction.php +++ b/app/Filament/Actions/Models/AssignHashidsAction.php @@ -50,7 +50,7 @@ class AssignHashidsAction extends BaseAction try { $action->assign($model, $this->connection); } catch (Exception $e) { - //$this->markAsFailed($model, $e); + $this->failedLog($e); } } } diff --git a/app/Filament/Actions/Models/Wiki/Anime/BackfillAnimeAction.php b/app/Filament/Actions/Models/Wiki/Anime/BackfillAnimeAction.php index b28fba376..ea79d14c8 100644 --- a/app/Filament/Actions/Models/Wiki/Anime/BackfillAnimeAction.php +++ b/app/Filament/Actions/Models/Wiki/Anime/BackfillAnimeAction.php @@ -83,7 +83,7 @@ class BackfillAnimeAction extends BaseAction implements ShouldQueue public function handle(Anime $anime, array $fields): void { if ($anime->resources()->doesntExist()) { - $this->fail(__('filament.actions.anime.backfill.message.resource_required_failure')); + $this->failedLog(__('filament.actions.anime.backfill.message.resource_required_failure')); return; } @@ -105,7 +105,7 @@ class BackfillAnimeAction extends BaseAction implements ShouldQueue } } } catch (Exception $e) { - $this->fail($e); + $this->failedLog($e); } finally { // Try not to upset third-party APIs Sleep::for(rand(3, 5))->second(); diff --git a/app/Filament/Actions/Models/Wiki/Studio/BackfillStudioAction.php b/app/Filament/Actions/Models/Wiki/Studio/BackfillStudioAction.php index c22b10c64..7916a59e5 100644 --- a/app/Filament/Actions/Models/Wiki/Studio/BackfillStudioAction.php +++ b/app/Filament/Actions/Models/Wiki/Studio/BackfillStudioAction.php @@ -16,7 +16,6 @@ use Filament\Forms\Components\Section; use Filament\Forms\Form; use Filament\Notifications\Actions\Action as NotificationAction; use Filament\Notifications\Notification; -use Filament\Support\Enums\MaxWidth; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\InteractsWithQueue; @@ -45,8 +44,6 @@ class BackfillStudioAction extends BaseAction implements ShouldQueue $this->label(__('filament.actions.studio.backfill.name')); - $this->modalWidth(MaxWidth::FourExtraLarge); - $this->authorize('update', Studio::class); $this->action(fn (Studio $record, array $data) => $this->handle($record, $data)); @@ -62,7 +59,7 @@ class BackfillStudioAction extends BaseAction implements ShouldQueue public function handle(Studio $studio, array $fields): void { if ($studio->resources()->doesntExist()) { - //$this->markAsFailed($studio, __('filament.actions.studio.backfill.message.resource_required_failure')); + $this->failedLog(__('filament.actions.studio.backfill.message.resource_required_failure')); return; } @@ -84,7 +81,7 @@ class BackfillStudioAction extends BaseAction implements ShouldQueue } } } catch (Exception $e) { - //$this->markAsFailed($studio, $e); + $this->failedLog($e); } finally { // Try not to upset third-party APIs Sleep::for(rand(3, 5))->second(); diff --git a/app/Filament/Actions/Models/Wiki/Video/BackfillAudioAction.php b/app/Filament/Actions/Models/Wiki/Video/BackfillAudioAction.php index b6b8e3ae2..99924261b 100644 --- a/app/Filament/Actions/Models/Wiki/Video/BackfillAudioAction.php +++ b/app/Filament/Actions/Models/Wiki/Video/BackfillAudioAction.php @@ -7,6 +7,7 @@ namespace App\Filament\Actions\Models\Wiki\Video; use App\Actions\Models\Wiki\Video\Audio\BackfillAudioAction as BackfillAudio; use App\Enums\Actions\Models\Wiki\Video\DeriveSourceVideo; use App\Enums\Actions\Models\Wiki\Video\OverwriteAudio; +use App\Filament\Actions\BaseAction; use App\Filament\Components\Fields\Select; use App\Models\Wiki\Audio; use App\Models\Wiki\Video; @@ -14,8 +15,6 @@ use Exception; use Filament\Forms\Form; use Filament\Notifications\Actions\Action as NotificationAction; use Filament\Notifications\Notification; -use Filament\Support\Enums\MaxWidth; -use Filament\Tables\Actions\Action; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\InteractsWithQueue; @@ -26,7 +25,7 @@ use Illuminate\Validation\Rules\Enum; /** * Class BackfillAudioAction. */ -class BackfillAudioAction extends Action implements ShouldQueue +class BackfillAudioAction extends BaseAction implements ShouldQueue { use InteractsWithQueue; use Queueable; @@ -45,10 +44,6 @@ class BackfillAudioAction extends Action implements ShouldQueue $this->label(__('filament.actions.video.backfill.name')); - $this->requiresConfirmation(); - - $this->modalWidth(MaxWidth::TwoExtraLarge); - $this->authorize('create', Audio::class); $this->action(fn (Video $record, array $data) => $this->handle($record, $data)); @@ -82,7 +77,7 @@ class BackfillAudioAction extends Action implements ShouldQueue ->sendToDatabase(Auth::user()); } } catch (Exception $e) { - //$this->markAsFailed($video, $e); + $this->failedLog($e); } } diff --git a/app/Filament/Actions/Storage/StorageAction.php b/app/Filament/Actions/Storage/StorageAction.php index 54c9aaf4b..3d08647a7 100644 --- a/app/Filament/Actions/Storage/StorageAction.php +++ b/app/Filament/Actions/Storage/StorageAction.php @@ -52,5 +52,9 @@ abstract class StorageAction extends BaseAction $action->then($storageResults); $actionResult = $storageResults->toActionResult(); + + if ($actionResult->hasFailed()) { + $this->failedLog($actionResult->getMessage()); + } } } diff --git a/app/Filament/BulkActions/Base/DeleteBulkAction.php b/app/Filament/BulkActions/Base/DeleteBulkAction.php new file mode 100644 index 000000000..9ab0e86ee --- /dev/null +++ b/app/Filament/BulkActions/Base/DeleteBulkAction.php @@ -0,0 +1,37 @@ +label(__('filament.bulk_actions.base.delete')); + + $this->authorize('forcedeleteany'); + + $this->after(function (DeleteBulkAction $action) { + foreach ($this->getRecords() as $record) { + $this->createActionLog($action, $record); + $this->finishedLog(); + } + }); + } +} diff --git a/app/Filament/BulkActions/Base/DetachBulkAction.php b/app/Filament/BulkActions/Base/DetachBulkAction.php new file mode 100644 index 000000000..c5eda1144 --- /dev/null +++ b/app/Filament/BulkActions/Base/DetachBulkAction.php @@ -0,0 +1,37 @@ +label(__('filament.bulk_actions.base.detach')); + + $this->authorize('forcedeleteany'); + + $this->after(function (DetachBulkAction $action) { + foreach ($this->getRecords() as $record) { + $this->createActionLog($action, $record); + $this->finishedLog(); + } + }); + } +} diff --git a/app/Filament/BulkActions/Base/ForceDeleteBulkAction.php b/app/Filament/BulkActions/Base/ForceDeleteBulkAction.php new file mode 100644 index 000000000..92ea87a15 --- /dev/null +++ b/app/Filament/BulkActions/Base/ForceDeleteBulkAction.php @@ -0,0 +1,32 @@ +label(__('filament.bulk_actions.base.forcedelete')); + + $this->hidden(false); + + $this->authorize('forcedeleteany'); + } +} diff --git a/app/Filament/BulkActions/Base/RestoreBulkAction.php b/app/Filament/BulkActions/Base/RestoreBulkAction.php new file mode 100644 index 000000000..12a82b523 --- /dev/null +++ b/app/Filament/BulkActions/Base/RestoreBulkAction.php @@ -0,0 +1,37 @@ +label(__('filament.bulk_actions.base.restore')); + + $this->authorize('forcedeleteany'); + + $this->after(function (RestoreBulkAction $action) { + foreach ($this->getRecords() as $record) { + $this->createActionLog($action, $record); + $this->finishedLog(); + } + }); + } +} diff --git a/app/Filament/BulkActions/BaseBulkAction.php b/app/Filament/BulkActions/BaseBulkAction.php index 4b878001d..bca44935b 100644 --- a/app/Filament/BulkActions/BaseBulkAction.php +++ b/app/Filament/BulkActions/BaseBulkAction.php @@ -4,7 +4,9 @@ declare(strict_types=1); namespace App\Filament\BulkActions; +use App\Concerns\Filament\Actions\HasActionLogs; use App\Models\BaseModel; +use Filament\Forms\Form; use Filament\Support\Enums\MaxWidth; use Filament\Tables\Actions\BulkAction; use Illuminate\Database\Eloquent\Collection; @@ -16,6 +18,8 @@ use Illuminate\Database\Eloquent\Collection; */ abstract class BaseBulkAction extends BulkAction { + use HasActionLogs; + /** * Initial setup for the action. * @@ -27,6 +31,14 @@ abstract class BaseBulkAction extends BulkAction $this->requiresConfirmation(); + $this->before(function ($action) { + foreach ($this->getRecords() as $record) { + $this->createActionLog($action, $record, false); + } + }); + + $this->after(fn () => $this->batchFinishedLog()); + $this->modalWidth(MaxWidth::FourExtraLarge); $this->action(fn (Collection $records, array $data) => $this->handle($records, $data)); diff --git a/app/Filament/Components/Columns/TextColumn.php b/app/Filament/Components/Columns/TextColumn.php index 0f2d376f4..ca9c84af8 100644 --- a/app/Filament/Components/Columns/TextColumn.php +++ b/app/Filament/Components/Columns/TextColumn.php @@ -8,6 +8,7 @@ use App\Filament\Resources\BaseResource; use App\Models\BaseModel; use Filament\Support\Enums\FontWeight; use Filament\Tables\Columns\TextColumn as ColumnsTextColumn; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; use Illuminate\Support\Str; @@ -30,7 +31,7 @@ class TextColumn extends ColumnsTextColumn return $this ->weight(FontWeight::SemiBold) ->html() - ->url(function (BaseModel $record) use ($resourceRelated, $relation, $shouldUseName, $limit) { + ->url(function (BaseModel|Model $record) use ($resourceRelated, $relation, $shouldUseName, $limit) { foreach (explode('.', $relation) as $element) { $record = Arr::get($record, $element); if ($record === null) return null; diff --git a/app/Filament/Components/Infolist/TextEntry.php b/app/Filament/Components/Infolist/TextEntry.php index d8ecb9e5e..d78bfd064 100644 --- a/app/Filament/Components/Infolist/TextEntry.php +++ b/app/Filament/Components/Infolist/TextEntry.php @@ -8,6 +8,7 @@ use App\Filament\Resources\BaseResource; use App\Models\BaseModel; use Filament\Infolists\Components\TextEntry as ComponentsTextEntry; use Filament\Support\Enums\FontWeight; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; /** @@ -28,7 +29,7 @@ class TextEntry extends ComponentsTextEntry return $this ->weight(FontWeight::SemiBold) ->html() - ->url(function (BaseModel $record) use ($resourceRelated, $relation, $shouldUseName) { + ->url(function (BaseModel|Model $record) use ($resourceRelated, $relation, $shouldUseName) { if (!empty($relation)) { foreach (explode('.', $relation) as $element) { $record = Arr::get($record, $element); diff --git a/app/Filament/HeaderActions/Base/CreateHeaderAction.php b/app/Filament/HeaderActions/Base/CreateHeaderAction.php new file mode 100644 index 000000000..a41059a73 --- /dev/null +++ b/app/Filament/HeaderActions/Base/CreateHeaderAction.php @@ -0,0 +1,23 @@ +label(__('filament.actions.base.delete')); + + $this->after(function ($record) { + ActionLog::modelDeleted($record); + }); + } +} diff --git a/app/Filament/HeaderActions/Base/EditHeaderAction.php b/app/Filament/HeaderActions/Base/EditHeaderAction.php new file mode 100644 index 000000000..eb0b0f196 --- /dev/null +++ b/app/Filament/HeaderActions/Base/EditHeaderAction.php @@ -0,0 +1,25 @@ +label(__('filament.actions.base.edit')); + } +} diff --git a/app/Filament/HeaderActions/Base/ForceDeleteHeaderAction.php b/app/Filament/HeaderActions/Base/ForceDeleteHeaderAction.php new file mode 100644 index 000000000..25618ac0c --- /dev/null +++ b/app/Filament/HeaderActions/Base/ForceDeleteHeaderAction.php @@ -0,0 +1,27 @@ +label(__('filament.actions.base.forcedelete')); + + $this->visible(true); + } +} diff --git a/app/Filament/HeaderActions/Base/RestoreHeaderAction.php b/app/Filament/HeaderActions/Base/RestoreHeaderAction.php new file mode 100644 index 000000000..078706106 --- /dev/null +++ b/app/Filament/HeaderActions/Base/RestoreHeaderAction.php @@ -0,0 +1,30 @@ +label(__('filament.actions.base.restore')); + + $this->after(function ($record) { + ActionLog::modelRestored($record); + }); + } +} diff --git a/app/Filament/HeaderActions/Base/ViewHeaderAction.php b/app/Filament/HeaderActions/Base/ViewHeaderAction.php new file mode 100644 index 000000000..b3ac8b3b4 --- /dev/null +++ b/app/Filament/HeaderActions/Base/ViewHeaderAction.php @@ -0,0 +1,28 @@ +label(__('filament.actions.base.view')); + + $this->hidden(fn ($livewire) => $livewire instanceof BaseViewResource); + } +} diff --git a/app/Filament/HeaderActions/BaseHeaderAction.php b/app/Filament/HeaderActions/BaseHeaderAction.php index 25383a3e3..4dba0f59a 100644 --- a/app/Filament/HeaderActions/BaseHeaderAction.php +++ b/app/Filament/HeaderActions/BaseHeaderAction.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Filament\HeaderActions; +use App\Concerns\Filament\Actions\HasActionLogs; use Filament\Actions\Action; use Filament\Support\Enums\MaxWidth; @@ -14,6 +15,8 @@ use Filament\Support\Enums\MaxWidth; */ abstract class BaseHeaderAction extends Action { + use HasActionLogs; + /** * Initial setup for the action. * @@ -25,6 +28,10 @@ abstract class BaseHeaderAction extends Action $this->requiresConfirmation(); + $this->afterFormValidated(fn (BaseHeaderAction $action) => $this->createActionLog($action)); + + $this->after(fn () => $this->finishedLog()); + $this->modalWidth(MaxWidth::FourExtraLarge); } } \ No newline at end of file diff --git a/app/Filament/HeaderActions/Discord/DiscordThreadHeaderAction.php b/app/Filament/HeaderActions/Discord/DiscordThreadHeaderAction.php index 4a56518be..7b285bc50 100644 --- a/app/Filament/HeaderActions/Discord/DiscordThreadHeaderAction.php +++ b/app/Filament/HeaderActions/Discord/DiscordThreadHeaderAction.php @@ -8,6 +8,7 @@ use App\Actions\Discord\DiscordThreadAction as DiscordThreadActionAction; use App\Filament\HeaderActions\BaseHeaderAction; use App\Models\Discord\DiscordThread; use App\Models\Wiki\Anime; +use Exception; use Filament\Forms\Components\TextInput; use Filament\Forms\Form; @@ -34,9 +35,15 @@ class DiscordThreadHeaderAction extends BaseHeaderAction $this->fillForm(fn (Anime $record): array => ['name' => $record->getName()]); - $this->action(fn (Anime $record, array $data) => (new DiscordThreadActionAction())->handle($record, $data)); + $this->action(function (Anime $record, array $data) { + $action = (new DiscordThreadActionAction())->handle($record, $data); + + if ($action instanceof Exception) { + $this->failedLog($action); + } + }); } - + /** * Get the fields available on the action. * @@ -55,4 +62,4 @@ class DiscordThreadHeaderAction extends BaseHeaderAction ->rules(['required', 'max:100']), ]); } -} \ No newline at end of file +} diff --git a/app/Filament/HeaderActions/Models/AssignHashidsHeaderAction.php b/app/Filament/HeaderActions/Models/AssignHashidsHeaderAction.php index 89a5584cf..8323860b3 100644 --- a/app/Filament/HeaderActions/Models/AssignHashidsHeaderAction.php +++ b/app/Filament/HeaderActions/Models/AssignHashidsHeaderAction.php @@ -50,7 +50,7 @@ class AssignHashidsHeaderAction extends BaseHeaderAction try { $action->assign($model, $this->connection); } catch (Exception $e) { - //$this->markAsFailed($model, $e); + $this->failedLog($e); } } } diff --git a/app/Filament/HeaderActions/Models/Wiki/Anime/BackfillAnimeHeaderAction.php b/app/Filament/HeaderActions/Models/Wiki/Anime/BackfillAnimeHeaderAction.php index 1324ef7d1..2cf21c433 100644 --- a/app/Filament/HeaderActions/Models/Wiki/Anime/BackfillAnimeHeaderAction.php +++ b/app/Filament/HeaderActions/Models/Wiki/Anime/BackfillAnimeHeaderAction.php @@ -80,10 +80,10 @@ class BackfillAnimeHeaderAction extends BaseHeaderAction implements ShouldQueue public function handle(Anime $anime, array $fields): void { if ($anime->resources()->doesntExist()) { - $this->fail(__('filament.actions.anime.backfill.message.resource_required_failure')); + $this->failedLog(__('filament.actions.anime.backfill.message.resource_required_failure')); return; } - + $actions = $this->getActions($fields, $anime); try { @@ -102,7 +102,7 @@ class BackfillAnimeHeaderAction extends BaseHeaderAction implements ShouldQueue } } } catch (Exception $e) { - $this->fail($e); + $this->failedLog($e); } finally { // Try not to upset third-party APIs Sleep::for(rand(3, 5))->second(); diff --git a/app/Filament/HeaderActions/Models/Wiki/Studio/BackfillStudioHeaderAction.php b/app/Filament/HeaderActions/Models/Wiki/Studio/BackfillStudioHeaderAction.php index 4768c3f0e..e761fd7f6 100644 --- a/app/Filament/HeaderActions/Models/Wiki/Studio/BackfillStudioHeaderAction.php +++ b/app/Filament/HeaderActions/Models/Wiki/Studio/BackfillStudioHeaderAction.php @@ -16,7 +16,6 @@ use Filament\Forms\Components\Section; use Filament\Forms\Form; use Filament\Notifications\Actions\Action as NotificationAction; use Filament\Notifications\Notification; -use Filament\Support\Enums\MaxWidth; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\InteractsWithQueue; @@ -45,8 +44,6 @@ class BackfillStudioHeaderAction extends BaseHeaderAction implements ShouldQueue $this->label(__('filament.actions.studio.backfill.name')); - $this->modalWidth(MaxWidth::FourExtraLarge); - $this->authorize('update', Studio::class); $this->action(fn (Studio $record, array $data) => $this->handle($record, $data)); @@ -62,7 +59,7 @@ class BackfillStudioHeaderAction extends BaseHeaderAction implements ShouldQueue public function handle(Studio $studio, array $fields): void { if ($studio->resources()->doesntExist()) { - //$this->markAsFailed($studio, __('filament.actions.studio.backfill.message.resource_required_failure')); + $this->failedLog(__('filament.actions.studio.backfill.message.resource_required_failure')); return; } @@ -84,7 +81,7 @@ class BackfillStudioHeaderAction extends BaseHeaderAction implements ShouldQueue } } } catch (Exception $e) { - //$this->markAsFailed($studio, $e); + $this->failedLog($e); } finally { // Try not to upset third-party APIs Sleep::for(rand(3, 5))->second(); diff --git a/app/Filament/HeaderActions/Models/Wiki/Video/BackfillAudioHeaderAction.php b/app/Filament/HeaderActions/Models/Wiki/Video/BackfillAudioHeaderAction.php index 1806de3d9..97233f377 100644 --- a/app/Filament/HeaderActions/Models/Wiki/Video/BackfillAudioHeaderAction.php +++ b/app/Filament/HeaderActions/Models/Wiki/Video/BackfillAudioHeaderAction.php @@ -80,7 +80,7 @@ class BackfillAudioHeaderAction extends BaseHeaderAction implements ShouldQueue ->sendToDatabase(Auth::user()); } } catch (Exception $e) { - //$this->markAsFailed($video, $e); + $this->failedLog($e); } } diff --git a/app/Filament/HeaderActions/Storage/StorageHeaderAction.php b/app/Filament/HeaderActions/Storage/StorageHeaderAction.php index efc7e0fa9..371bd99b2 100644 --- a/app/Filament/HeaderActions/Storage/StorageHeaderAction.php +++ b/app/Filament/HeaderActions/Storage/StorageHeaderAction.php @@ -52,5 +52,9 @@ abstract class StorageHeaderAction extends BaseHeaderAction $action->then($storageResults); $actionResult = $storageResults->toActionResult(); + + if ($actionResult->hasFailed()) { + $this->failedLog($actionResult->getMessage()); + } } } diff --git a/app/Filament/RelationManagers/Base/ActionLogRelationManager.php b/app/Filament/RelationManagers/Base/ActionLogRelationManager.php new file mode 100644 index 000000000..1d4e41dad --- /dev/null +++ b/app/Filament/RelationManagers/Base/ActionLogRelationManager.php @@ -0,0 +1,67 @@ +defaultSort(ActionLogModel::ATTRIBUTE_ID, 'desc') + ->heading(__('filament.resources.label.action_logs')) + ->pluralModelLabel(__('filament.resources.label.action_logs')) + ->columns(ActionLog::table($table)->getColumns()) + ->paginationPageOptions([5, 10, 25]) + ->defaultPaginationPageOption(5) + ->actions([ + ViewAction::make() + ->form(fn (Form $form) => ActionLog::form($form)), + ]); + } + + /** + * Get the filters available for the relation. + * + * @return array + * + * @noinspection PhpMissingParentCallCommonInspection + */ + public static function getFilters(): array + { + return []; + } + + /** + * Determine whether the related model can be created. + * + * @return bool + */ + protected function canCreate(): bool + { + return false; + } +} diff --git a/app/Filament/RelationManagers/BaseRelationManager.php b/app/Filament/RelationManagers/BaseRelationManager.php index 3e233b824..be84b2e92 100644 --- a/app/Filament/RelationManagers/BaseRelationManager.php +++ b/app/Filament/RelationManagers/BaseRelationManager.php @@ -4,26 +4,13 @@ declare(strict_types=1); namespace App\Filament\RelationManagers; +use App\Filament\Actions\Base\AttachAction; +use App\Filament\Actions\Base\CreateAction; +use App\Filament\BulkActions\Base\DetachBulkAction; use App\Filament\Components\Columns\TextColumn; -use App\Filament\Components\Fields\Select; -use App\Filament\RelationManagers\Wiki\ResourceRelationManager; -use App\Filament\Resources\Wiki\Artist\RelationManagers\SongArtistRelationManager; -use App\Filament\Resources\Wiki\ExternalResource\RelationManagers\AnimeResourceRelationManager; -use App\Filament\Resources\Wiki\ExternalResource\RelationManagers\ArtistResourceRelationManager; -use App\Filament\Resources\Wiki\ExternalResource\RelationManagers\SongResourceRelationManager; -use App\Filament\Resources\Wiki\ExternalResource\RelationManagers\StudioResourceRelationManager; -use App\Filament\Resources\Wiki\Song\RelationManagers\ArtistSongRelationManager; -use App\Models\BaseModel; use App\Pivots\BasePivot; -use App\Pivots\Wiki\AnimeResource; -use App\Pivots\Wiki\ArtistSong; use DateTime; -use Filament\Forms\Components\TextInput; -use Filament\Forms\Form; use Filament\Resources\RelationManagers\RelationManager; -use Filament\Tables\Actions\AttachAction; -use Filament\Tables\Actions\CreateAction; -use Filament\Tables\Actions\DetachBulkAction; use Filament\Tables\Filters\TrashedFilter; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Relations\BelongsToMany; @@ -63,17 +50,21 @@ abstract class BaseRelationManager extends RelationManager TextColumn::make(BasePivot::ATTRIBUTE_CREATED_AT) ->label(__('filament.fields.base.created_at')) ->hidden(fn ($livewire) => !($livewire->getRelationship() instanceof BelongsToMany)) - ->formatStateUsing(function (BaseModel $record) { + ->formatStateUsing(function ($record) { $pivot = current($record->getRelations()); - return (new DateTime(Arr::get($pivot->getAttributes(), BasePivot::ATTRIBUTE_CREATED_AT)))->format('M j, Y H:i:s'); + $createdAtField = Arr::get($pivot->getAttributes(), BasePivot::ATTRIBUTE_CREATED_AT); + if (!$createdAtField) return '-'; + return (new DateTime($createdAtField))->format('M j, Y H:i:s'); }), TextColumn::make(BasePivot::ATTRIBUTE_UPDATED_AT) ->label(__('filament.fields.base.updated_at')) ->hidden(fn ($livewire) => !($livewire->getRelationship() instanceof BelongsToMany)) - ->formatStateUsing(function (BaseModel $record) { + ->formatStateUsing(function ($record) { $pivot = current($record->getRelations()); - return (new DateTime(Arr::get($pivot->getAttributes(), BasePivot::ATTRIBUTE_UPDATED_AT)))->format('M j, Y H:i:s'); + $updatedAtField = Arr::get($pivot->getAttributes(), BasePivot::ATTRIBUTE_UPDATED_AT); + if (!$updatedAtField) return '-'; + return (new DateTime($updatedAtField))->format('M j, Y H:i:s'); }), ], )) @@ -122,9 +113,7 @@ abstract class BaseRelationManager extends RelationManager public static function getBulkActions(): array { return [ - DetachBulkAction::make() - ->label(__('filament.bulk_actions.base.detach')) - ->authorize('forcedeleteany'), + DetachBulkAction::make(), ]; } @@ -140,42 +129,7 @@ abstract class BaseRelationManager extends RelationManager return [ CreateAction::make(), - AttachAction::make() - ->hidden(fn (BaseRelationManager $livewire) => !($livewire->getRelationship() instanceof BelongsToMany)) - ->authorize('create') - ->recordSelect(function (BaseRelationManager $livewire) { - /** @var string */ - $model = $livewire->getTable()->getModel(); - $title = $livewire->getTable()->getRecordTitle(new $model); - return Select::make('recordId') - ->label($title) - ->useScout($model); - }) - ->form(function (Form $form, AttachAction $action) { - return $form - ->schema([ - $action->getRecordSelect(), - - TextInput::make(AnimeResource::ATTRIBUTE_AS) - ->label(__('filament.fields.anime.resources.as.name')) - ->helperText(__('filament.fields.anime.resources.as.help')) - ->visibleOn([ - AnimeResourceRelationManager::class, - ArtistResourceRelationManager::class, - SongResourceRelationManager::class, - StudioResourceRelationManager::class, - ResourceRelationManager::class, - ]), - - TextInput::make(ArtistSong::ATTRIBUTE_AS) - ->label(__('filament.fields.artist.songs.as.name')) - ->helperText(__('filament.fields.artist.songs.as.help')) - ->visibleOn([ - ArtistSongRelationManager::class, - SongArtistRelationManager::class, - ]), - ]); - }), + AttachAction::make(), ]; } } diff --git a/app/Filament/RelationManagers/Wiki/ImageRelationManager.php b/app/Filament/RelationManagers/Wiki/ImageRelationManager.php index 3f6a14e65..12d137c03 100644 --- a/app/Filament/RelationManagers/Wiki/ImageRelationManager.php +++ b/app/Filament/RelationManagers/Wiki/ImageRelationManager.php @@ -107,4 +107,14 @@ abstract class ImageRelationManager extends BaseRelationManager ImageResource::getHeaderActions(), ); } + + /** + * Determine whether the related model can be created. + * + * @return bool + */ + protected function canCreate(): bool + { + return false; + } } diff --git a/app/Filament/RelationManagers/Wiki/Video/ScriptRelationManager.php b/app/Filament/RelationManagers/Wiki/Video/ScriptRelationManager.php index 14a68210b..98d3bcc79 100644 --- a/app/Filament/RelationManagers/Wiki/Video/ScriptRelationManager.php +++ b/app/Filament/RelationManagers/Wiki/Video/ScriptRelationManager.php @@ -107,4 +107,14 @@ abstract class ScriptRelationManager extends BaseRelationManager ScriptResource::getHeaderActions(), ); } + + /** + * Determine whether the related model can be created. + * + * @return bool + */ + protected function canCreate(): bool + { + return false; + } } diff --git a/app/Filament/RelationManagers/Wiki/VideoRelationManager.php b/app/Filament/RelationManagers/Wiki/VideoRelationManager.php index b4cf5356d..12a3bdf6a 100644 --- a/app/Filament/RelationManagers/Wiki/VideoRelationManager.php +++ b/app/Filament/RelationManagers/Wiki/VideoRelationManager.php @@ -107,4 +107,14 @@ abstract class VideoRelationManager extends BaseRelationManager VideoResource::getHeaderActions(), ); } + + /** + * Determine whether the related model can be created. + * + * @return bool + */ + protected function canCreate(): bool + { + return false; + } } diff --git a/app/Filament/Resources/Admin/ActionLog.php b/app/Filament/Resources/Admin/ActionLog.php new file mode 100644 index 000000000..f8ee1294f --- /dev/null +++ b/app/Filament/Resources/Admin/ActionLog.php @@ -0,0 +1,394 @@ +schema([ + Textarea::make(ActionLogModel::ATTRIBUTE_EXCEPTION) + ->label(__('filament.fields.action_log.exception')) + ->disabled() + ->placeholder('-') + ->columnSpanFull(), + ]); + } + + /** + * The index page of the resource. + * + * @param Table $table + * @return Table + * + * @noinspection PhpMissingParentCallCommonInspection + */ + public static function table(Table $table): Table + { + return parent::table($table) + ->columns([ + TextColumn::make(ActionLogModel::ATTRIBUTE_ID) + ->label(__('filament.fields.base.id')) + ->sortable(), + + TextColumn::make(ActionLogModel::ATTRIBUTE_NAME) + ->label(__('filament.fields.action_log.name')) + ->sortable() + ->searchable(), + + TextColumn::make(ActionLogModel::ATTRIBUTE_USER) + ->label(__('filament.resources.singularLabel.user')) + ->urlToRelated(User::class, ActionLogModel::RELATION_USER, true) + ->sortable(), + + TextColumn::make(ActionLogModel::ATTRIBUTE_TARGET) + ->label(__('filament.fields.action_log.target')) + ->formatStateUsing(fn ($state) => class_basename($state) . ': ' . $state->getName()) + ->sortable(), + + TextColumn::make(ActionLogModel::ATTRIBUTE_STATUS) + ->label(__('filament.fields.action_log.status')) + ->formatStateUsing(fn (ActionLogStatus $state) => $state->localize()) + ->color(fn (ActionLogStatus $state) => $state->color()) + ->badge() + ->sortable(), + + TextColumn::make(BaseModel::ATTRIBUTE_CREATED_AT) + ->label(__('filament.fields.action_log.happened_at')) + ->dateTime() + ->sortable(), + + TextColumn::make(ActionLogModel::ATTRIBUTE_FINISHED_AT) + ->label(__('filament.fields.action_log.finished_at')) + ->dateTime() + ->sortable() + ->placeholder('-'), + ]); + } + + /** + * Get the infolist available for the resource. + * + * @param Infolist $infolist + * @return Infolist + * + * @noinspection PhpMissingParentCallCommonInspection + */ + public static function infolist(Infolist $infolist): Infolist + { + return $infolist + ->schema([ + Section::make(static::getRecordTitle($infolist->getRecord())) + ->schema([ + TextEntry::make(ActionLogModel::ATTRIBUTE_ID) + ->label(__('filament.fields.base.id')), + + TextEntry::make(ActionLogModel::ATTRIBUTE_NAME) + ->label(__('filament.fields.action_log.name')) + ->formatStateUsing(fn ($state) => ucfirst($state)), + + TextEntry::make(ActionLogModel::ATTRIBUTE_USER) + ->label(__('filament.resources.singularLabel.user')) + ->urlToRelated(User::class, ActionLogModel::RELATION_USER, true), + + TextEntry::make(ActionLogModel::ATTRIBUTE_TARGET) + ->label(__('filament.fields.action_log.target')) + ->formatStateUsing(fn ($state) => class_basename($state) . ': ' . $state->getName()), + + TextEntry::make(ActionLogModel::ATTRIBUTE_STATUS) + ->label(__('filament.fields.action_log.status')) + ->formatStateUsing(fn (ActionLogStatus $state) => $state->localize()) + ->color(fn (ActionLogStatus $state) => $state->color()) + ->badge(), + + TextEntry::make(BaseModel::ATTRIBUTE_CREATED_AT) + ->label(__('filament.fields.action_log.happened_at')) + ->dateTime(), + + TextEntry::make(ActionLogModel::ATTRIBUTE_FINISHED_AT) + ->label(__('filament.fields.action_log.finished_at')) + ->dateTime() + ->placeholder('-'), + + TextEntry::make(ActionLogModel::ATTRIBUTE_EXCEPTION) + ->label(__('filament.fields.action_log.exception')) + ->placeholder('-') + ->columnSpanFull(), + ]) + ->columns(3), + + Section::make(__('filament.fields.base.timestamps')) + ->schema(parent::timestamps()) + ->columns(3), + ]); + } + + /** + * Get the relationships available for the resource. + * + * @return array + * + * @noinspection PhpMissingParentCallCommonInspection + */ + public static function getRelations(): array + { + return [ + RelationGroup::make( + static::getLabel(), + [], + ), + ]; + } + + /** + * Get the filters available for the resource. + * + * @return array + * + * @noinspection PhpMissingParentCallCommonInspection + */ + public static function getFilters(): array + { + return [ + SelectFilter::make(ActionLogModel::ATTRIBUTE_STATUS) + ->label(__('filament.fields.action_log.status')) + ->options(ActionLogStatus::asSelectArray()), + + DateFilter::make(BaseModel::ATTRIBUTE_CREATED_AT) + ->labels(__('filament.filters.action_log.happened_at_from'), __('filament.filters.action_log.happened_at_to')), + + DateFilter::make(ActionLogModel::ATTRIBUTE_FINISHED_AT) + ->labels(__('filament.filters.action_log.finished_at_from'), __('filament.filters.action_log.finished_at_to')), + ]; + } + + /** + * Get the actions available for the resource. + * + * @return array + * + * @noinspection PhpMissingParentCallCommonInspection + */ + public static function getActions(): array + { + return array_merge( + parent::getActions(), + [], + ); + } + + /** + * Get the bulk actions available for the resource. + * + * @return array + * + * @noinspection PhpMissingParentCallCommonInspection + */ + public static function getBulkActions(): array + { + return array_merge( + parent::getBulkActions(), + [], + ); + } + + /** + * Get the header actions available for the resource. + * + * @return array + * + * @noinspection PhpMissingParentCallCommonInspection + */ + public static function getHeaderActions(): array + { + return array_merge( + parent::getHeaderActions(), + [], + ); + } + + /** + * Determine whether the related model can be created. + * + * @return bool + */ + public static function canCreate(): bool + { + return false; + } + + /** + * Determine whether the related model can be edited. + * + * @param Model $record + * @return bool + */ + public static function canEdit(Model $record): bool + { + return false; + } + + /** + * Determine whether the related model can be deleted. + * + * @param Model $record + * @return bool + */ + public static function canDelete(Model $record): bool + { + return false; + } + + /** + * Determine whether the related model can be force-deleted. + * + * @param Model $record + * @return bool + */ + public static function canForceDelete(Model $record): bool + { + return false; + } + + /** + * Determine if the user can access the table. + * + * @return bool + */ + public static function canAccess(): bool + { + /** @var UserModel */ + $user = Filament::auth()->user(); + return $user->hasRole(Role::ADMIN->value); + } + + /** + * Get the pages available for the resource. + * + * @return array + * + * @noinspection PhpMissingParentCallCommonInspection + */ + public static function getPages(): array + { + return [ + 'index' => ListActionLogs::route('/'), + 'view' => ViewActionLog::route('/{record:id}'), + ]; + } +} diff --git a/app/Filament/Resources/Admin/ActionLog/Pages/ListActionLogs.php b/app/Filament/Resources/Admin/ActionLog/Pages/ListActionLogs.php new file mode 100644 index 000000000..69005b77c --- /dev/null +++ b/app/Filament/Resources/Admin/ActionLog/Pages/ListActionLogs.php @@ -0,0 +1,31 @@ +getRecord()); + } } diff --git a/app/Filament/Resources/Base/BaseEditResource.php b/app/Filament/Resources/Base/BaseEditResource.php index eb774a30f..b92603d9b 100644 --- a/app/Filament/Resources/Base/BaseEditResource.php +++ b/app/Filament/Resources/Base/BaseEditResource.php @@ -4,11 +4,12 @@ declare(strict_types=1); namespace App\Filament\Resources\Base; +use App\Filament\HeaderActions\Base\DeleteHeaderAction; +use App\Filament\HeaderActions\Base\ForceDeleteHeaderAction; +use App\Filament\HeaderActions\Base\RestoreHeaderAction; +use App\Filament\HeaderActions\Base\ViewHeaderAction; +use App\Models\Admin\ActionLog; use Filament\Actions\ActionGroup; -use Filament\Actions\DeleteAction; -use Filament\Actions\ForceDeleteAction; -use Filament\Actions\RestoreAction; -use Filament\Actions\ViewAction; use Filament\Resources\Pages\EditRecord; /** @@ -26,23 +27,28 @@ abstract class BaseEditResource extends EditRecord protected function getHeaderActions(): array { return [ - ViewAction::make() - ->label(__('filament.actions.base.view')) - ->hidden(fn ($livewire) => $livewire instanceof BaseViewResource), + ViewHeaderAction::make(), ActionGroup::make([ - DeleteAction::make() + DeleteHeaderAction::make() ->label(__('filament.actions.base.delete')), - ForceDeleteAction::make() - ->label(__('filament.actions.base.forcedelete')) - ->visible(true), + ForceDeleteHeaderAction::make(), ]) ->icon('heroicon-o-trash') ->color('danger'), - RestoreAction::make() - ->label(__('filament.actions.base.restore')), + RestoreHeaderAction::make(), ]; } + + /** + * Run after the record is edited. + * + * @return void + */ + protected function afterSave(): void + { + ActionLog::modelUpdated($this->getRecord()); + } } diff --git a/app/Filament/Resources/Base/BaseListResources.php b/app/Filament/Resources/Base/BaseListResources.php index db9f5a814..a4cd27a2b 100644 --- a/app/Filament/Resources/Base/BaseListResources.php +++ b/app/Filament/Resources/Base/BaseListResources.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Filament\Resources\Base; -use Filament\Actions\CreateAction; +use App\Filament\HeaderActions\Base\CreateHeaderAction; use Filament\Resources\Pages\ListRecords; /** @@ -22,7 +22,7 @@ abstract class BaseListResources extends ListRecords protected function getHeaderActions(): array { return [ - CreateAction::make(), + CreateHeaderAction::make(), ]; } } diff --git a/app/Filament/Resources/Base/BaseViewResource.php b/app/Filament/Resources/Base/BaseViewResource.php index 0558caf45..78d5bc84d 100644 --- a/app/Filament/Resources/Base/BaseViewResource.php +++ b/app/Filament/Resources/Base/BaseViewResource.php @@ -4,8 +4,9 @@ declare(strict_types=1); namespace App\Filament\Resources\Base; -use Filament\Actions\EditAction; +use App\Filament\HeaderActions\Base\EditHeaderAction; use Filament\Resources\Pages\ViewRecord; +use Illuminate\Support\Arr; /** * Class BaseViewResource. @@ -21,14 +22,21 @@ class BaseViewResource extends ViewRecord */ protected function getHeaderActions(): array { - $editPage = (new static::$resource)::getPages()['edit']->getPage(); + $pages = (new static::$resource)::getPages(); + + if (Arr::has($pages, 'edit')) { + $editPage = $pages['edit']->getPage(); + $action = (new $editPage)->getHeaderActions(); + } else { + $action = []; + } return array_merge( [ - EditAction::make() - ->label(__('filament.actions.base.edit')), + EditHeaderAction::make() + ->visible(Arr::has($pages, 'edit')), ], - (new $editPage)->getHeaderActions(), + $action, ); } } diff --git a/app/Filament/Resources/BaseResource.php b/app/Filament/Resources/BaseResource.php index 134d7e603..2baede0c2 100644 --- a/app/Filament/Resources/BaseResource.php +++ b/app/Filament/Resources/BaseResource.php @@ -4,22 +4,22 @@ declare(strict_types=1); namespace App\Filament\Resources; +use App\Filament\Actions\Base\DeleteAction; +use App\Filament\Actions\Base\DetachAction; +use App\Filament\Actions\Base\EditAction; +use App\Filament\Actions\Base\ForceDeleteAction; +use App\Filament\Actions\Base\RestoreAction; +use App\Filament\Actions\Base\ViewAction; +use App\Filament\BulkActions\Base\DeleteBulkAction; +use App\Filament\BulkActions\Base\ForceDeleteBulkAction; +use App\Filament\BulkActions\Base\RestoreBulkAction; use App\Filament\Components\Filters\DateFilter; -use App\Filament\RelationManagers\BaseRelationManager; +use App\Filament\RelationManagers\Base\ActionLogRelationManager; use App\Models\BaseModel; use Filament\Infolists\Components\TextEntry; use Filament\Resources\Resource; use Filament\Tables\Actions\ActionGroup; use Filament\Tables\Actions\BulkActionGroup; -use Filament\Tables\Actions\DeleteAction; -use Filament\Tables\Actions\DeleteBulkAction; -use Filament\Tables\Actions\DetachAction; -use Filament\Tables\Actions\EditAction; -use Filament\Tables\Actions\ForceDeleteAction; -use Filament\Tables\Actions\ForceDeleteBulkAction; -use Filament\Tables\Actions\RestoreAction; -use Filament\Tables\Actions\RestoreBulkAction; -use Filament\Tables\Actions\ViewAction; use Filament\Tables\Filters\TrashedFilter; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; @@ -121,30 +121,21 @@ abstract class BaseResource extends Resource public static function getActions(): array { return [ - ViewAction::make() - ->label(__('filament.actions.base.view')), + ViewAction::make(), - EditAction::make() - ->label(__('filament.actions.base.edit')), + EditAction::make(), ActionGroup::make([ - DetachAction::make() - ->label(__('filament.actions.base.detach')) - ->hidden(fn ($livewire) => !($livewire instanceof BaseRelationManager)) - ->authorize('delete'), + DetachAction::make(), - DeleteAction::make() - ->label(__('filament.actions.base.delete')), + DeleteAction::make(), - ForceDeleteAction::make() - ->label(__('filament.actions.base.forcedelete')) - ->visible(true), + ForceDeleteAction::make(), ]) ->icon('heroicon-o-trash') ->color('danger'), - RestoreAction::make() - ->label(__('filament.actions.base.restore')), + RestoreAction::make(), ]; } @@ -159,18 +150,11 @@ abstract class BaseResource extends Resource { return [ BulkActionGroup::make([ - DeleteBulkAction::make() - ->label(__('filament.bulk_actions.base.delete')) - ->authorize('delete', (new static::$model)), + DeleteBulkAction::make(), - ForceDeleteBulkAction::make() - ->label(__('filament.bulk_actions.base.forcedelete')) - ->hidden(false) - ->authorize('forcedelete', (new static::$model)), + ForceDeleteBulkAction::make(), - RestoreBulkAction::make() - ->label(__('filament.bulk_actions.base.restore')) - ->authorize('restore', (new static::$model)), + RestoreBulkAction::make(), ]), ]; } @@ -202,6 +186,20 @@ abstract class BaseResource extends Resource ]); } + /** + * Get the base relationships available for all resources. + * + * @return array + * + * @noinspection PhpMissingParentCallCommonInspection + */ + public static function getBaseRelations(): array + { + return [ + ActionLogRelationManager::class, + ]; + } + /** * Get the generic slug (URI key) for the resource. * diff --git a/app/Filament/Resources/Discord/DiscordThread.php b/app/Filament/Resources/Discord/DiscordThread.php index f11fee79b..56b4fbeb5 100644 --- a/app/Filament/Resources/Discord/DiscordThread.php +++ b/app/Filament/Resources/Discord/DiscordThread.php @@ -24,6 +24,7 @@ use Filament\Forms\Form; use Filament\Forms\Set; use Filament\Infolists\Components\Section; use Filament\Infolists\Infolist; +use Filament\Resources\RelationManagers\RelationGroup; use Filament\Tables\Actions\ActionGroup; use Filament\Tables\Table; use Illuminate\Support\Arr; @@ -228,7 +229,14 @@ class DiscordThread extends BaseResource */ public static function getRelations(): array { - return []; + return [ + RelationGroup::make(static::getLabel(), + array_merge( + [], + parent::getBaseRelations(), + ) + ), + ]; } /** diff --git a/app/Filament/Resources/Document/Page.php b/app/Filament/Resources/Document/Page.php index fcb720e41..771b2e1c9 100644 --- a/app/Filament/Resources/Document/Page.php +++ b/app/Filament/Resources/Document/Page.php @@ -21,6 +21,7 @@ use Filament\Forms\Get; use Filament\Forms\Set; use Filament\Infolists\Components\Section; use Filament\Infolists\Infolist; +use Filament\Resources\RelationManagers\RelationGroup; use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; @@ -299,7 +300,14 @@ class Page extends BaseResource */ public static function getRelations(): array { - return []; + return [ + RelationGroup::make(static::getLabel(), + array_merge( + [], + parent::getBaseRelations(), + ) + ), + ]; } /** diff --git a/app/Filament/Resources/List/Playlist.php b/app/Filament/Resources/List/Playlist.php index 7da761213..bfaf1254b 100644 --- a/app/Filament/Resources/List/Playlist.php +++ b/app/Filament/Resources/List/Playlist.php @@ -301,10 +301,15 @@ class Playlist extends BaseResource public static function getRelations(): array { return [ - RelationGroup::make(static::getLabel(), [ - ImagePlaylistRelationManager::class, - TrackPlaylistRelationManager::class, - ]), + RelationGroup::make(static::getLabel(), + array_merge( + [ + ImagePlaylistRelationManager::class, + TrackPlaylistRelationManager::class, + ], + parent::getBaseRelations(), + ) + ), ]; } diff --git a/app/Filament/Resources/List/Playlist/Track.php b/app/Filament/Resources/List/Playlist/Track.php index d08e81b5f..fe21412fe 100644 --- a/app/Filament/Resources/List/Playlist/Track.php +++ b/app/Filament/Resources/List/Playlist/Track.php @@ -23,6 +23,7 @@ use Filament\Forms\Components\TextInput; use Filament\Forms\Form; use Filament\Infolists\Components\Section; use Filament\Infolists\Infolist; +use Filament\Resources\RelationManagers\RelationGroup; use Filament\Tables\Table; /** @@ -241,7 +242,14 @@ class Track extends BaseResource */ public static function getRelations(): array { - return []; + return [ + RelationGroup::make(static::getLabel(), + array_merge( + [], + parent::getBaseRelations(), + ) + ), + ]; } /** diff --git a/app/Filament/Resources/Wiki/Anime.php b/app/Filament/Resources/Wiki/Anime.php index 5d23d9e15..5b295a10b 100644 --- a/app/Filament/Resources/Wiki/Anime.php +++ b/app/Filament/Resources/Wiki/Anime.php @@ -352,14 +352,19 @@ class Anime extends BaseResource public static function getRelations(): array { return [ - RelationGroup::make(static::getLabel(), [ - SynonymAnimeRelationManager::class, - ThemeAnimeRelationManager::class, - SeriesAnimeRelationManager::class, - ResourceAnimeRelationManager::class, - ImageAnimeRelationManager::class, - StudioAnimeRelationManager::class, - ]), + RelationGroup::make(static::getLabel(), + array_merge( + [ + SynonymAnimeRelationManager::class, + ThemeAnimeRelationManager::class, + SeriesAnimeRelationManager::class, + ResourceAnimeRelationManager::class, + ImageAnimeRelationManager::class, + StudioAnimeRelationManager::class, + ], + parent::getBaseRelations(), + ) + ), ]; } diff --git a/app/Filament/Resources/Wiki/Anime/Synonym.php b/app/Filament/Resources/Wiki/Anime/Synonym.php index f6939b5f7..8f8831f9b 100644 --- a/app/Filament/Resources/Wiki/Anime/Synonym.php +++ b/app/Filament/Resources/Wiki/Anime/Synonym.php @@ -22,6 +22,7 @@ use Filament\Forms\Components\TextInput; use Filament\Forms\Form; use Filament\Infolists\Components\Section; use Filament\Infolists\Infolist; +use Filament\Resources\RelationManagers\RelationGroup; use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; use Illuminate\Validation\Rules\Enum; @@ -228,7 +229,14 @@ class Synonym extends BaseResource */ public static function getRelations(): array { - return []; + return [ + RelationGroup::make(static::getLabel(), + array_merge( + [], + parent::getBaseRelations(), + ) + ), + ]; } /** diff --git a/app/Filament/Resources/Wiki/Anime/Theme.php b/app/Filament/Resources/Wiki/Anime/Theme.php index 46dab7434..2f31f2dc5 100644 --- a/app/Filament/Resources/Wiki/Anime/Theme.php +++ b/app/Filament/Resources/Wiki/Anime/Theme.php @@ -235,6 +235,7 @@ class Theme extends BaseResource Repeater::make(ThemeModel::RELATION_SONG . '.' . Song::RELATION_ARTISTS) ->label(__('filament.resources.label.artists')) + ->addActionLabel(__('filament.buttons.add').' '.__('filament.resources.singularLabel.artist')) ->hidden(fn (Get $get) => $get(ThemeModel::ATTRIBUTE_SONG) === null) ->live(true) ->key('song.artists') @@ -292,6 +293,7 @@ class Theme extends BaseResource ->schema([ Repeater::make(ThemeModel::RELATION_ENTRIES) ->label(__('filament.resources.label.anime_theme_entries')) + ->addActionLabel(__('filament.buttons.add')) ->relationship() ->schema(Entry::form($form)->getComponents()), ]), @@ -471,9 +473,14 @@ class Theme extends BaseResource public static function getRelations(): array { return [ - RelationGroup::make(static::getLabel(), [ - EntryThemeRelationManager::class, - ]), + RelationGroup::make(static::getLabel(), + array_merge( + [ + EntryThemeRelationManager::class, + ], + parent::getBaseRelations(), + ) + ), ]; } diff --git a/app/Filament/Resources/Wiki/Anime/Theme/Entry.php b/app/Filament/Resources/Wiki/Anime/Theme/Entry.php index ed909c970..f7701f2f7 100644 --- a/app/Filament/Resources/Wiki/Anime/Theme/Entry.php +++ b/app/Filament/Resources/Wiki/Anime/Theme/Entry.php @@ -337,9 +337,14 @@ class Entry extends BaseResource public static function getRelations(): array { return [ - RelationGroup::make(static::getLabel(), [ - VideoEntryRelationManager::class, - ]), + RelationGroup::make(static::getLabel(), + array_merge( + [ + VideoEntryRelationManager::class, + ], + parent::getBaseRelations(), + ) + ), ]; } diff --git a/app/Filament/Resources/Wiki/Artist.php b/app/Filament/Resources/Wiki/Artist.php index c24387f89..8f02c8988 100644 --- a/app/Filament/Resources/Wiki/Artist.php +++ b/app/Filament/Resources/Wiki/Artist.php @@ -277,13 +277,18 @@ class Artist extends BaseResource public static function getRelations(): array { return [ - RelationGroup::make(static::getLabel(), [ - SongArtistRelationManager::class, - ResourceArtistRelationManager::class, - MemberArtistRelationManager::class, - GroupArtistRelationManager::class, - ImageArtistRelationManager::class, - ]), + RelationGroup::make(static::getLabel(), + array_merge( + [ + SongArtistRelationManager::class, + ResourceArtistRelationManager::class, + MemberArtistRelationManager::class, + GroupArtistRelationManager::class, + ImageArtistRelationManager::class, + ], + parent::getBaseRelations(), + ) + ), ]; } diff --git a/app/Filament/Resources/Wiki/Audio.php b/app/Filament/Resources/Wiki/Audio.php index aeeae5ae0..c34f9e934 100644 --- a/app/Filament/Resources/Wiki/Audio.php +++ b/app/Filament/Resources/Wiki/Audio.php @@ -12,7 +12,6 @@ use App\Filament\Components\Columns\TextColumn; use App\Filament\Components\Filters\NumberFilter; use App\Filament\Components\Infolist\TextEntry; use App\Filament\Resources\BaseResource; -use App\Filament\Resources\Wiki\Audio\Pages\CreateAudio; use App\Filament\Resources\Wiki\Audio\Pages\EditAudio; use App\Filament\Resources\Wiki\Audio\Pages\ListAudios; use App\Filament\Resources\Wiki\Audio\Pages\ViewAudio; @@ -219,9 +218,14 @@ class Audio extends BaseResource public static function getRelations(): array { return [ - RelationGroup::make(static::getLabel(), [ - VideoAudioRelationManager::class, - ]), + RelationGroup::make(static::getLabel(), + array_merge( + [ + VideoAudioRelationManager::class, + ], + parent::getBaseRelations(), + ) + ), ]; } @@ -301,6 +305,16 @@ class Audio extends BaseResource ]; } + /** + * Determine whether the related model can be created. + * + * @return bool + */ + public static function canCreate(): bool + { + return false; + } + /** * Get the pages available for the resource. * @@ -312,7 +326,6 @@ class Audio extends BaseResource { return [ 'index' => ListAudios::route('/'), - 'create' => CreateAudio::route('/create'), 'view' => ViewAudio::route('/{record:audio_id}'), 'edit' => EditAudio::route('/{record:audio_id}/edit'), ]; diff --git a/app/Filament/Resources/Wiki/Audio/Pages/CreateAudio.php b/app/Filament/Resources/Wiki/Audio/Pages/CreateAudio.php deleted file mode 100644 index c25ec397b..000000000 --- a/app/Filament/Resources/Wiki/Audio/Pages/CreateAudio.php +++ /dev/null @@ -1,16 +0,0 @@ - ListImages::route('/'), - 'create' => CreateImage::route('/create'), 'view' => ViewImage::route('/{record:image_id}'), 'edit' => EditImage::route('/{record:image_id}/edit'), ]; diff --git a/app/Filament/Resources/Wiki/Image/Pages/CreateImage.php b/app/Filament/Resources/Wiki/Image/Pages/CreateImage.php deleted file mode 100644 index 76c8433c2..000000000 --- a/app/Filament/Resources/Wiki/Image/Pages/CreateImage.php +++ /dev/null @@ -1,16 +0,0 @@ - ListVideos::route('/'), - 'create' => CreateVideo::route('/create'), 'view' => ViewVideo::route('/{record:video_id}'), 'edit' => EditVideo::route("/{record:video_id}/edit"), ]; diff --git a/app/Filament/Resources/Wiki/Video/Pages/CreateVideo.php b/app/Filament/Resources/Wiki/Video/Pages/CreateVideo.php deleted file mode 100644 index 19654470b..000000000 --- a/app/Filament/Resources/Wiki/Video/Pages/CreateVideo.php +++ /dev/null @@ -1,16 +0,0 @@ - ListScripts::route('/'), - 'create' => CreateScript::route('/create'), 'view' => ViewScript::route('/{record:script_id}'), 'edit' => EditScript::route('/{record:script_id}/edit'), ]; diff --git a/app/Filament/Resources/Wiki/Video/Script/Pages/CreateScript.php b/app/Filament/Resources/Wiki/Video/Script/Pages/CreateScript.php deleted file mode 100644 index f3ad78dfc..000000000 --- a/app/Filament/Resources/Wiki/Video/Script/Pages/CreateScript.php +++ /dev/null @@ -1,16 +0,0 @@ -requiresConfirmation(); + $this->afterFormValidated(function ($livewire, BaseTableAction $action) { + if ($livewire instanceof BaseRelationManager) { + $this->createActionLog($action, $livewire->getOwnerRecord()); + } + }); + + $this->after(function ($livewire, BaseTableAction $action) { + if ($livewire instanceof BaseRelationManager) { + $this->finishedLog(); + } + }); + $this->modalWidth(MaxWidth::FourExtraLarge); $this->action(fn (array $data) => $this->handle($data)); diff --git a/app/Filament/TableActions/Models/Discord/DiscordEditMessageTableAction.php b/app/Filament/TableActions/Models/Discord/DiscordEditMessageTableAction.php index 9bcc97a80..15c8c1e35 100644 --- a/app/Filament/TableActions/Models/Discord/DiscordEditMessageTableAction.php +++ b/app/Filament/TableActions/Models/Discord/DiscordEditMessageTableAction.php @@ -112,6 +112,7 @@ class DiscordEditMessageTableAction extends BaseTableAction Repeater::make(DiscordMessage::ATTRIBUTE_EMBEDS) ->label(__('filament.table_actions.discord_thread.message.embeds.name')) + ->addActionLabel(__('filament.buttons.add')) ->helperText(__('filament.table_actions.discord_thread.message.embeds.help')) ->key(DiscordMessage::ATTRIBUTE_EMBEDS) ->collapsible() @@ -141,6 +142,7 @@ class DiscordEditMessageTableAction extends BaseTableAction Repeater::make(DiscordEmbed::ATTRIBUTE_FIELDS) ->label(__('filament.table_actions.discord_thread.message.embeds.body.fields.title.name')) + ->addActionLabel(__('filament.buttons.add')) ->helperText(__('filament.table_actions.discord_thread.message.embeds.body.fields.title.help')) ->collapsible() ->schema([ @@ -164,6 +166,7 @@ class DiscordEditMessageTableAction extends BaseTableAction Repeater::make(DiscordMessage::ATTRIBUTE_IMAGES) ->label(__('filament.table_actions.discord_thread.message.images.name')) + ->addActionLabel(__('filament.buttons.add')) ->helperText(__('filament.table_actions.discord_thread.message.images.help')) ->key(DiscordMessage::ATTRIBUTE_IMAGES) ->collapsible() diff --git a/app/Filament/TableActions/Models/Discord/DiscordSendMessageTableAction.php b/app/Filament/TableActions/Models/Discord/DiscordSendMessageTableAction.php index 8691f5887..0e1c90598 100644 --- a/app/Filament/TableActions/Models/Discord/DiscordSendMessageTableAction.php +++ b/app/Filament/TableActions/Models/Discord/DiscordSendMessageTableAction.php @@ -73,6 +73,7 @@ class DiscordSendMessageTableAction extends BaseTableAction Repeater::make(DiscordMessage::ATTRIBUTE_EMBEDS) ->label(__('filament.table_actions.discord_thread.message.embeds.name')) + ->addActionLabel(__('filament.buttons.add')) ->helperText(__('filament.table_actions.discord_thread.message.embeds.help')) ->collapsible() ->defaultItems(0) @@ -101,6 +102,7 @@ class DiscordSendMessageTableAction extends BaseTableAction Repeater::make(DiscordEmbed::ATTRIBUTE_FIELDS) ->label(__('filament.table_actions.discord_thread.message.embeds.body.fields.title.name')) + ->addActionLabel(__('filament.buttons.add')) ->helperText(__('filament.table_actions.discord_thread.message.embeds.body.fields.title.help')) ->collapsible() ->schema([ @@ -124,6 +126,7 @@ class DiscordSendMessageTableAction extends BaseTableAction Repeater::make(DiscordMessage::ATTRIBUTE_IMAGES) ->label(__('filament.table_actions.discord_thread.message.images.name')) + ->addActionLabel(__('filament.buttons.add')) ->helperText(__('filament.table_actions.discord_thread.message.images.help')) ->collapsible() ->defaultItems(0) diff --git a/app/Filament/TableActions/Storage/StorageTableAction.php b/app/Filament/TableActions/Storage/StorageTableAction.php index a909bf3ef..7a7c6802d 100644 --- a/app/Filament/TableActions/Storage/StorageTableAction.php +++ b/app/Filament/TableActions/Storage/StorageTableAction.php @@ -5,7 +5,9 @@ declare(strict_types=1); namespace App\Filament\TableActions\Storage; use App\Contracts\Actions\Storage\StorageAction as BaseStorageAction; +use App\Filament\RelationManagers\BaseRelationManager; use App\Filament\TableActions\BaseTableAction; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; /** * Class StorageTableAction. @@ -34,8 +36,27 @@ abstract class StorageTableAction extends BaseTableAction $storageResults->toLog(); - $action->then($storageResults); + $model = $action->then($storageResults); $actionResult = $storageResults->toActionResult(); + + if ($actionResult->hasFailed()) { + $this->failedLog($actionResult->getMessage()); + return; + } + + $livewire = $this->getLivewire(); + if ($livewire instanceof BaseRelationManager) { + /** @var BelongsToMany */ + $relation = $livewire->getRelationship(); + $pivotClass = $relation->getPivotClass(); + + $pivot = $pivotClass::query() + ->where($livewire->getOwnerRecord()->getKeyName(), $livewire->getOwnerRecord()->getKey()) + ->where($model->getKeyName(), $model->getKey()) + ->first(); + + $this->updateLog($model, $pivot); + } } } diff --git a/app/Models/Admin/ActionLog.php b/app/Models/Admin/ActionLog.php new file mode 100644 index 000000000..f66a3791c --- /dev/null +++ b/app/Models/Admin/ActionLog.php @@ -0,0 +1,408 @@ + + */ + protected $fillable = [ + ActionLog::ATTRIBUTE_BATCH_ID, + ActionLog::ATTRIBUTE_NAME, + ActionLog::ATTRIBUTE_USER, + ActionLog::ATTRIBUTE_ACTIONABLE_TYPE, + ActionLog::ATTRIBUTE_ACTIONABLE_ID, + ActionLog::ATTRIBUTE_TARGET_TYPE, + ActionLog::ATTRIBUTE_TARGET_ID, + ActionLog::ATTRIBUTE_MODEL_TYPE, + ActionLog::ATTRIBUTE_MODEL_ID, + ActionLog::ATTRIBUTE_STATUS, + ActionLog::ATTRIBUTE_EXCEPTION, + ActionLog::ATTRIBUTE_FINISHED_AT, + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + ActionLog::ATTRIBUTE_STATUS => ActionLogStatus::class, + ]; + + /** + * The table associated with the model. + * + * @var string + */ + protected $table = ActionLog::TABLE; + + /** + * The primary key associated with the table. + * + * @var string + */ + protected $primaryKey = ActionLog::ATTRIBUTE_ID; + + /** + * Get name. + * + * @return string + */ + public function getName(): string + { + return Str::of($this->name) + ->append(' - ') + ->append($this->target()->getName()) + ->__toString(); + } + + /** + * Get subtitle. + * + * @return string + */ + public function getSubtitle(): string + { + return $this->name; + } + + /** + * Get the actionable. + * + * @return MorphTo + */ + public function actionable(): MorphTo + { + return $this->morphTo(); + } + + /** + * Get the user that initiated the action. + * + * @return BelongsTo + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class, ActionLog::ATTRIBUTE_USER); + } + + /** + * Get the target of the action for user interface linking. + * + * @return MorphTo|BaseModel + */ + public function target(): MorphTo|BaseModel + { + return $this + ->morphTo(ActionLog::ATTRIBUTE_TARGET, ActionLog::ATTRIBUTE_TARGET_TYPE, ActionLog::ATTRIBUTE_TARGET_ID) + ->withTrashed(); + } + + /** + * Get the user for the action log. + * + * @return User + */ + public static function getUser(): User + { + return Filament::auth()->user(); + } + + /** + * Register an action log for a model created. + * + * @param Model $model + * @return ActionLog + */ + public static function modelCreated(Model $model): ActionLog + { + return ActionLog::query()->create([ + ActionLog::ATTRIBUTE_BATCH_ID => (string) Str::orderedUuid(), + ActionLog::ATTRIBUTE_USER => ActionLog::getUser()->getKey(), + ActionLog::ATTRIBUTE_NAME => 'Create', + ActionLog::ATTRIBUTE_ACTIONABLE_TYPE => $model->getMorphClass(), + ActionLog::ATTRIBUTE_ACTIONABLE_ID => $model->getKey(), + ActionLog::ATTRIBUTE_TARGET_TYPE => $model->getMorphClass(), + ActionLog::ATTRIBUTE_TARGET_ID => $model->getKey(), + ActionLog::ATTRIBUTE_MODEL_TYPE => $model->getMorphClass(), + ActionLog::ATTRIBUTE_MODEL_ID => $model->getKey(), + ActionLog::ATTRIBUTE_STATUS => ActionLogStatus::FINISHED->value, + ActionLog::ATTRIBUTE_FINISHED_AT => Date::now(), + ]); + } + + /** + * Register an action log for a model updated. + * + * @param Model $model + * @return ActionLog + */ + public static function modelUpdated(Model $model): ActionLog + { + return ActionLog::query()->create([ + ActionLog::ATTRIBUTE_BATCH_ID => (string) Str::orderedUuid(), + ActionLog::ATTRIBUTE_USER => ActionLog::getUser()->getKey(), + ActionLog::ATTRIBUTE_NAME => 'Update', + ActionLog::ATTRIBUTE_ACTIONABLE_TYPE => $model->getMorphClass(), + ActionLog::ATTRIBUTE_ACTIONABLE_ID => $model->getKey(), + ActionLog::ATTRIBUTE_TARGET_TYPE => $model->getMorphClass(), + ActionLog::ATTRIBUTE_TARGET_ID => $model->getKey(), + ActionLog::ATTRIBUTE_MODEL_TYPE => $model->getMorphClass(), + ActionLog::ATTRIBUTE_MODEL_ID => $model->getKey(), + ActionLog::ATTRIBUTE_STATUS => ActionLogStatus::FINISHED->value, + ActionLog::ATTRIBUTE_FINISHED_AT => Date::now(), + ]); + } + + /** + * Register an action log for a model deleted. + * + * @param Model $model + * @return ActionLog + */ + public static function modelDeleted(Model $model): ActionLog + { + return ActionLog::modelSoftDeleted('Delete', $model); + } + + /** + * Register an action log for a model restored. + * + * @param Model $model + * @return ActionLog + */ + public static function modelRestored(Model $model): ActionLog + { + return ActionLog::modelSoftDeleted('Restore', $model); + } + + /** + * Register an action log for a model that is soft-deleted. + * + * @param string $actionName + * @param Model $model + * @return ActionLog + */ + public static function modelSoftDeleted(string $actionName, Model $model): ActionLog + { + return ActionLog::query()->create([ + ActionLog::ATTRIBUTE_BATCH_ID => Str::orderedUuid()->__toString(), + ActionLog::ATTRIBUTE_USER => ActionLog::getUser()->getKey(), + ActionLog::ATTRIBUTE_NAME => $actionName, + ActionLog::ATTRIBUTE_ACTIONABLE_TYPE => $model->getMorphClass(), + ActionLog::ATTRIBUTE_ACTIONABLE_ID => $model->getKey(), + ActionLog::ATTRIBUTE_TARGET_TYPE => $model->getMorphClass(), + ActionLog::ATTRIBUTE_TARGET_ID => $model->getKey(), + ActionLog::ATTRIBUTE_MODEL_TYPE => $model->getMorphClass(), + ActionLog::ATTRIBUTE_MODEL_ID => $model->getKey(), + ActionLog::ATTRIBUTE_STATUS => ActionLogStatus::FINISHED->value, + ActionLog::ATTRIBUTE_FINISHED_AT => Date::now(), + ]); + } + + /** + * Register an action log for a model attached. + * + * @param string $action + * @param Model $related + * @param Model $parent + * @param Model $pivot + * @return ActionLog + */ + public static function modelPivot(string $action, Model $related, Model $parent, Model $pivot): ActionLog + { + return ActionLog::query()->create([ + ActionLog::ATTRIBUTE_BATCH_ID => Str::orderedUuid()->__toString(), + ActionLog::ATTRIBUTE_USER => ActionLog::getUser()->getKey(), + ActionLog::ATTRIBUTE_NAME => $action, + ActionLog::ATTRIBUTE_ACTIONABLE_TYPE => $related->getMorphClass(), + ActionLog::ATTRIBUTE_ACTIONABLE_ID => $related->getKey(), + ActionLog::ATTRIBUTE_TARGET_TYPE => $parent->getMorphClass(), + ActionLog::ATTRIBUTE_TARGET_ID => $parent->getKey(), + ActionLog::ATTRIBUTE_MODEL_TYPE => $pivot->getMorphClass(), + ActionLog::ATTRIBUTE_MODEL_ID => $pivot->getKey(), + ActionLog::ATTRIBUTE_STATUS => ActionLogStatus::FINISHED->value, + ActionLog::ATTRIBUTE_FINISHED_AT => Date::now(), + ]); + } + + /** + * Register an action log for when a model has an action executed. + * + * @param string $batchId + * @param mixed $action + * @param Model $model + * @return ActionLog + */ + public static function modelActioned(string $batchId, mixed $action, Model $model): ActionLog + { + return ActionLog::query()->create([ + ActionLog::ATTRIBUTE_BATCH_ID => $batchId, + ActionLog::ATTRIBUTE_USER => ActionLog::getUser()->getKey(), + ActionLog::ATTRIBUTE_NAME => $action->getLabel(), + ActionLog::ATTRIBUTE_ACTIONABLE_TYPE => $model->getMorphClass(), + ActionLog::ATTRIBUTE_ACTIONABLE_ID => $model->getKey(), + ActionLog::ATTRIBUTE_TARGET_TYPE => $model->getMorphClass(), + ActionLog::ATTRIBUTE_TARGET_ID => $model->getKey(), + ActionLog::ATTRIBUTE_MODEL_TYPE => $model->getMorphClass(), + ActionLog::ATTRIBUTE_MODEL_ID => $model->getKey(), + ActionLog::ATTRIBUTE_STATUS => ActionLogStatus::RUNNING->value, + ]); + } + + /** + * Update the all the models status of a batch to running. + * + * @return void + */ + public function batchRunning(): void + { + $this->query()->where(ActionLog::ATTRIBUTE_BATCH_ID, $this->batch_id) + ->whereNotIn(ActionLog::ATTRIBUTE_STATUS, [ActionLogStatus::FINISHED->value, ActionLogStatus::FAILED->value]) + ->update([ + ActionLog::ATTRIBUTE_STATUS => ActionLogStatus::RUNNING->value, + ]); + } + + /** + * Update the all the models status of a batch to finished. + * + * @return void + */ + public function batchFinished(): void + { + $this->query()->where(ActionLog::ATTRIBUTE_BATCH_ID, $this->batch_id) + ->whereNotIn(ActionLog::ATTRIBUTE_STATUS, [ActionLogStatus::FINISHED->value, ActionLogStatus::FAILED->value]) + ->update([ + ActionLog::ATTRIBUTE_STATUS => ActionLogStatus::FINISHED->value, + ActionLog::ATTRIBUTE_FINISHED_AT => Date::now(), + ]); + } + + /** + * Update the all the models status of a batch to failed. + * + * @param Throwable|string|null $exception + * @return void + */ + public function batchFailed(Throwable|string|null $exception = null): void + { + $this->query()->where(ActionLog::ATTRIBUTE_BATCH_ID, $this->batch_id) + ->whereNotIn(ActionLog::ATTRIBUTE_STATUS, [ActionLogStatus::FINISHED->value, ActionLogStatus::FAILED->value]) + ->update([ + ActionLog::ATTRIBUTE_STATUS => ActionLogStatus::FAILED->value, + ActionLog::ATTRIBUTE_EXCEPTION => $exception ? Str::of($exception)->__toString() : null, + ActionLog::ATTRIBUTE_FINISHED_AT => Date::now(), + ]); + } + + /** + * Update the model status to finished. + * + * @return void + */ + public function finished(): void + { + $this->updateStatus(ActionLogStatus::FINISHED); + } + + /** + * Update the model status to failed. + * + * @param Throwable|string|null $exception + * @return void + */ + public function failed(Throwable|string|null $exception = null): void + { + $this->updateStatus(ActionLogStatus::FAILED, $exception); + } + + /** + * Check if the model status is failed. + * + * @return bool + */ + public function isFailed(): bool + { + return $this->status === ActionLogStatus::FAILED; + } + + /** + * Update the status of a given action event. + * + * @param ActionLogStatus $status + * @param Throwable|string|null $exception + * @return void + */ + public function updateStatus(ActionLogStatus $status, Throwable|string|null $exception = null): void + { + $this->update([ + ActionLog::ATTRIBUTE_STATUS => $status->value, + ActionLog::ATTRIBUTE_EXCEPTION => Str::of($exception)->__toString(), + ActionLog::ATTRIBUTE_FINISHED_AT => Date::now(), + ]); + } +} diff --git a/app/Models/Auth/User.php b/app/Models/Auth/User.php index 06f33aeb4..cc92e6298 100644 --- a/app/Models/Auth/User.php +++ b/app/Models/Auth/User.php @@ -11,6 +11,7 @@ use App\Events\Auth\User\UserCreated; use App\Events\Auth\User\UserDeleted; use App\Events\Auth\User\UserRestored; use App\Events\Auth\User\UserUpdated; +use App\Models\Admin\ActionLog; use App\Models\List\Playlist; use Database\Factories\Auth\UserFactory; use Filament\Models\Contracts\FilamentUser; @@ -223,4 +224,14 @@ class User extends Authenticatable implements MustVerifyEmail, Nameable, HasSubt { return $this->hasMany(Playlist::class, Playlist::ATTRIBUTE_USER); } + + /** + * Get the action logs the user executed. + * + * @return HasMany + */ + public function action_logs(): HasMany + { + return $this->hasMany(ActionLog::class, ActionLog::ATTRIBUTE_USER); + } } diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php index d0b333f58..1186871e7 100644 --- a/app/Models/BaseModel.php +++ b/app/Models/BaseModel.php @@ -7,10 +7,12 @@ namespace App\Models; use App\Contracts\Models\Nameable; use App\Contracts\Models\HasSubtitle; use App\Enums\Http\Api\Filter\ComparisonOperator; +use App\Models\Admin\ActionLog; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Prunable; +use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Config; @@ -100,4 +102,14 @@ abstract class BaseModel extends Model implements Nameable, HasSubtitle now()->subWeek() ); } + + /** + * Get the action logs for the model. + * + * @return MorphMany + */ + public function action_logs(): MorphMany + { + return $this->morphMany(ActionLog::class, 'actionable'); + } } diff --git a/database/migrations/2024_06_25_145723_create_action_logs_table.php b/database/migrations/2024_06_25_145723_create_action_logs_table.php new file mode 100644 index 000000000..e38c5344b --- /dev/null +++ b/database/migrations/2024_06_25_145723_create_action_logs_table.php @@ -0,0 +1,48 @@ +bigIncrements(ActionLog::ATTRIBUTE_ID); + $table->string(ActionLog::ATTRIBUTE_BATCH_ID); + + $table->unsignedBigInteger(ActionLog::ATTRIBUTE_USER); + $table->foreign(ActionLog::ATTRIBUTE_USER)->references(User::ATTRIBUTE_ID)->on(User::TABLE)->cascadeOnDelete(); + + $table->string(ActionLog::ATTRIBUTE_NAME); + $table->morphs(ActionLog::ATTRIBUTE_ACTIONABLE); + $table->morphs(ActionLog::ATTRIBUTE_TARGET); + $table->string(ActionLog::ATTRIBUTE_MODEL_TYPE); + $table->uuid(ActionLog::ATTRIBUTE_MODEL_ID)->nullable(); + $table->integer(ActionLog::ATTRIBUTE_STATUS)->nullable(); + $table->text(ActionLog::ATTRIBUTE_EXCEPTION)->nullable(); + $table->timestamps(6); + $table->timestamp(ActionLog::ATTRIBUTE_FINISHED_AT, 6)->nullable(); + + $table->index([ActionLog::ATTRIBUTE_BATCH_ID, ActionLog::ATTRIBUTE_MODEL_TYPE, ActionLog::ATTRIBUTE_MODEL_ID]); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists(ActionLog::TABLE); + } +}; diff --git a/lang/en/enums.php b/lang/en/enums.php index 86cdf796f..bd9be3812 100644 --- a/lang/en/enums.php +++ b/lang/en/enums.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use App\Enums\Actions\ActionLogStatus; use App\Enums\Models\List\PlaylistVisibility; use App\Enums\Models\Wiki\AnimeMediaFormat; use App\Enums\Models\Wiki\AnimeSeason; @@ -34,6 +35,11 @@ return [ AnimeSynonymType::ENGLISH->name => 'English', AnimeSynonymType::SHORT->name => 'Short', ], + ActionLogStatus::class => [ + ActionLogStatus::RUNNING->name => 'Running', + ActionLogStatus::FAILED->name => 'Failed', + ActionLogStatus::FINISHED->name => 'Finished', + ], ImageFacet::class => [ ImageFacet::COVER_SMALL->name => 'Small Cover', ImageFacet::COVER_LARGE->name => 'Large Cover', diff --git a/lang/en/filament.php b/lang/en/filament.php index fdd92cec4..5f9376f39 100644 --- a/lang/en/filament.php +++ b/lang/en/filament.php @@ -433,6 +433,9 @@ return [ ], ], ], + 'buttons' => [ + 'add' => 'Add', + ], 'dashboards' => [ 'icon' => [ 'admin' => 'heroicon-m-chart-bar', @@ -444,6 +447,14 @@ return [ ], ], 'fields' => [ + 'action_log' => [ + 'name' => 'Name', + 'target' => 'Target', + 'status' => 'Status', + 'happened_at' => 'Happened At', + 'finished_at' => 'Finished At', + 'exception' => 'Exception', + ], 'anime_synonym' => [ 'text' => [ 'help' => 'For alternative titles, licensed titles, common abbreviations and/or shortenings', @@ -822,6 +833,12 @@ return [ ], ], 'filters' => [ + 'action_log' => [ + 'finished_at_from' => 'Finished At - From', + 'finished_at_to' => 'Finished At - To', + 'happened_at_from' => 'Happened At - From', + 'happened_at_to' => 'Happened At - To', + ], 'anime' => [ 'year_from' => 'Year - From', 'year_to' => 'Year - To', @@ -871,6 +888,7 @@ return [ 'wiki' => 'Wiki', ], 'icon' => [ + 'action_logs' => 'heroicon-o-rectangle-stack', 'anime_synonyms' => 'heroicon-o-globe-alt', 'anime_theme_entries' => 'heroicon-o-list-bullet', 'anime_themes' => 'heroicon-o-list-bullet', @@ -899,6 +917,7 @@ return [ 'videos' => 'heroicon-o-film', ], 'label' => [ + 'action_logs' => 'Action Logs', 'anime_synonyms' => 'Anime Synonyms', 'anime_theme_entries' => 'Anime Theme Entries', 'anime_themes' => 'Anime Themes', @@ -927,6 +946,7 @@ return [ 'videos' => 'Videos', ], 'singularLabel' => [ + 'action_log' => 'Action Log', 'anime_synonym' => 'Anime Synonym', 'anime_theme_entry' => 'Anime Theme Entry', 'anime_theme' => 'Anime Theme', diff --git a/phpstan.neon b/phpstan.neon index f3daf3ff6..0cedc6ea6 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -28,3 +28,6 @@ parameters: - message: '#Right side of && is always true.#' path: app/Providers/RouteServiceProvider.php + - + message: '#Call to an undefined method App\\Filament\\BulkActions\\.*::getRecord\(\).#' + path: app/Concerns/Filament/Actions/HasActionLogs.php