feat: added admin dumps (#807)

This commit is contained in:
Kyrch
2025-05-04 00:47:26 -03:00
committed by GitHub
parent d468811adc
commit efbdcc1e6a
95 changed files with 1137 additions and 283 deletions
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace App\Actions\Storage\Admin\Dump;
use App\Concerns\Repositories\Admin\ReconcilesDumpRepositories;
use App\Models\Admin\ActionLog;
use App\Models\Admin\Announcement;
use App\Models\Admin\Dump;
use App\Models\Admin\Feature;
use App\Models\Admin\FeaturedTheme;
use App\Models\Service\View;
use App\Models\Service\ViewAggregate;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
/**
* Class DumpAdminAction.
*/
class DumpAdminAction extends DumpAction
{
use ReconcilesDumpRepositories;
final public const FILENAME_PREFIX = 'animethemes-db-dump-admin-';
/**
* The list of tables to include in the dump.
*
* @return array
*/
protected function allowedTables(): array
{
return [
'action_events', // Nova events
ActionLog::TABLE,
Announcement::TABLE,
Dump::TABLE,
Feature::TABLE,
FeaturedTheme::TABLE,
ViewAggregate::TABLE,
View::TABLE,
];
}
/**
* The temporary path for the database dump.
* Note: The dumper library does not support writing to disk, so we have to write to the local filesystem first.
* Pattern: "animethemes-db-dump-admin-{milliseconds from epoch}.sql".
*
* @return string
*/
protected function getDumpFile(): string
{
$filesystem = Storage::disk('local');
return Str::of($filesystem->path(''))
->append(DumpAdminAction::FILENAME_PREFIX)
->append(strval(Date::now()->valueOf()))
->append('.sql')
->__toString();
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace App\Actions\Storage\Admin\Dump;
use App\Concerns\Repositories\Admin\ReconcilesDumpRepositories;
use App\Models\Auth\Permission;
use App\Models\Auth\Role;
use App\Models\Auth\User;
use App\Models\List\External\ExternalToken;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
/**
* Class DumpAuthAction.
*/
class DumpAuthAction extends DumpAction
{
use ReconcilesDumpRepositories;
final public const FILENAME_PREFIX = 'animethemes-db-dump-auth-';
/**
* The list of tables to include in the dump.
*
* @return array
*/
protected function allowedTables(): array
{
return [
// This table stores tokens which are sensitive data.
ExternalToken::TABLE,
Permission::TABLE,
Role::TABLE,
User::TABLE,
'model_has_permissions',
'model_has_roles',
'password_reset_tokens',
'personal_access_tokens',
'role_has_permissions',
];
}
/**
* The temporary path for the database dump.
* Note: The dumper library does not support writing to disk, so we have to write to the local filesystem first.
* Pattern: "animethemes-db-dump-auth-{milliseconds from epoch}.sql".
*
* @return string
*/
protected function getDumpFile(): string
{
$filesystem = Storage::disk('local');
return Str::of($filesystem->path(''))
->append(DumpAuthAction::FILENAME_PREFIX)
->append(strval(Date::now()->valueOf()))
->append('.sql')
->__toString();
}
}
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Actions\Storage\Admin\Dump;
use App\Concerns\Repositories\Admin\ReconcilesDumpRepositories;
use App\Models\Discord\DiscordThread;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
/**
* Class DumpDiscordAction.
*/
class DumpDiscordAction extends DumpAction
{
use ReconcilesDumpRepositories;
final public const FILENAME_PREFIX = 'animethemes-db-dump-discord-';
/**
* The list of tables to include in the dump.
*
* @return array
*/
protected function allowedTables(): array
{
return [
DiscordThread::TABLE,
];
}
/**
* The temporary path for the database dump.
* Note: The dumper library does not support writing to disk, so we have to write to the local filesystem first.
* Pattern: "animethemes-db-dump-discord-{milliseconds from epoch}.sql".
*
* @return string
*/
protected function getDumpFile(): string
{
$filesystem = Storage::disk('local');
return Str::of($filesystem->path(''))
->append(DumpDiscordAction::FILENAME_PREFIX)
->append(strval(Date::now()->valueOf()))
->append('.sql')
->__toString();
}
}
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace App\Actions\Storage\Admin\Dump;
use App\Concerns\Repositories\Admin\ReconcilesDumpRepositories;
use App\Models\List\External\ExternalEntry;
use App\Models\List\ExternalProfile;
use App\Models\List\Playlist;
use App\Models\List\Playlist\PlaylistTrack;
use App\Pivots\List\PlaylistImage;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
/**
* Class DumpListAction.
*/
class DumpListAction extends DumpAction
{
use ReconcilesDumpRepositories;
final public const FILENAME_PREFIX = 'animethemes-db-dump-list-';
/**
* The list of tables to include in the dump.
*
* @return array
*/
protected function allowedTables(): array
{
return [
ExternalEntry::TABLE,
ExternalProfile::TABLE,
PlaylistTrack::TABLE,
Playlist::TABLE,
PlaylistImage::TABLE,
];
}
/**
* The temporary path for the database dump.
* Note: The dumper library does not support writing to disk, so we have to write to the local filesystem first.
* Pattern: "animethemes-db-dump-list-{milliseconds from epoch}.sql".
*
* @return string
*/
protected function getDumpFile(): string
{
$filesystem = Storage::disk('local');
return Str::of($filesystem->path(''))
->append(DumpListAction::FILENAME_PREFIX)
->append(strval(Date::now()->valueOf()))
->append('.sql')
->__toString();
}
}
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Actions\Storage\Admin\Dump;
use App\Concerns\Repositories\Admin\ReconcilesDumpRepositories;
use App\Models\User\Encode;
use App\Models\User\Notification;
use App\Models\User\Report;
use App\Models\User\Report\ReportStep;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
/**
* Class DumpUserAction.
*/
class DumpUserAction extends DumpAction
{
use ReconcilesDumpRepositories;
final public const FILENAME_PREFIX = 'animethemes-db-dump-user-';
/**
* The list of tables to include in the dump.
*
* @return array
*/
protected function allowedTables(): array
{
return [
Encode::TABLE,
Notification::TABLE,
Report::TABLE,
ReportStep::TABLE,
];
}
/**
* The temporary path for the database dump.
* Note: The dumper library does not support writing to disk, so we have to write to the local filesystem first.
* Pattern: "animethemes-db-dump-user-{milliseconds from epoch}.sql".
*
* @return string
*/
protected function getDumpFile(): string
{
$filesystem = Storage::disk('local');
return Str::of($filesystem->path(''))
->append(DumpUserAction::FILENAME_PREFIX)
->append(strval(Date::now()->valueOf()))
->append('.sql')
->__toString();
}
}
@@ -55,8 +55,7 @@ trait DiscordThreadActionTrait
->label(__('filament.actions.discord.thread.name'))
->helperText(__('filament.actions.discord.thread.help'))
->required()
->maxlength(100)
->rules(['required', 'max:100']),
->maxlength(100),
]);
}
}
@@ -66,8 +66,7 @@ trait GiveRoleActionTrait
->label(__('filament.resources.singularLabel.role'))
->searchable()
->required()
->options($roles)
->rules('required'),
->options($roles),
]);
}
}
@@ -66,8 +66,7 @@ trait RevokeRoleActionTrait
->label(__('filament.resources.singularLabel.role'))
->searchable()
->required()
->options($roles)
->rules('required'),
->options($roles),
]);
}
}
@@ -66,8 +66,7 @@ trait GivePermissionActionTrait
->label(__('filament.resources.singularLabel.permission'))
->searchable()
->required()
->options($permissions)
->rules('required'),
->options($permissions),
]);
}
}
@@ -66,8 +66,7 @@ trait RevokePermissionActionTrait
->label(__('filament.resources.singularLabel.permission'))
->searchable()
->required()
->options($permissions)
->rules('required'),
->options($permissions),
]);
}
}
@@ -66,8 +66,7 @@ trait GivePermissionActionTrait
->label(__('filament.resources.singularLabel.permission'))
->searchable()
->required()
->options($permissions)
->rules('required'),
->options($permissions),
]);
}
}
@@ -66,8 +66,7 @@ trait GiveRoleActionTrait
->label(__('filament.resources.singularLabel.role'))
->searchable()
->required()
->options($roles)
->rules('required'),
->options($roles),
]);
}
}
@@ -66,8 +66,7 @@ trait RevokePermissionActionTrait
->label(__('filament.resources.singularLabel.permission'))
->searchable()
->required()
->options($permissions)
->rules('required'),
->options($permissions),
]);
}
}
@@ -66,8 +66,7 @@ trait RevokeRoleActionTrait
->label(__('filament.resources.singularLabel.role'))
->searchable()
->required()
->options($roles)
->rules('required'),
->options($roles),
]);
}
}
@@ -74,7 +74,7 @@ trait AttachResourceActionTrait
->helperText(__("filament.actions.models.wiki.attach_resource.fields.{$resourceSiteLower}.help"))
->url()
->maxLength(192)
->rules(['max:192', $resourceSite->getFormatRule($model)]);
->rule($resourceSite->getFormatRule($model));
}
return $form
@@ -19,7 +19,6 @@ use Illuminate\Bus\Queueable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rules\Enum;
/**
* Trait BackfillAudioActionTrait.
@@ -96,21 +95,24 @@ trait BackfillAudioActionTrait
->label(__('filament.actions.video.backfill.fields.derive_source.name'))
->helperText(__('filament.actions.video.backfill.fields.derive_source.help'))
->options(DeriveSourceVideo::asSelectArray())
->rules(['required', new Enum(DeriveSourceVideo::class)])
->required()
->enum(DeriveSourceVideo::class)
->default(DeriveSourceVideo::YES->value),
Select::make(OverwriteAudio::getFieldKey())
->label(__('filament.actions.video.backfill.fields.overwrite.name'))
->helperText(__('filament.actions.video.backfill.fields.overwrite.help'))
->options(OverwriteAudio::asSelectArray())
->rules(['required', new Enum(OverwriteAudio::class)])
->required()
->enum(OverwriteAudio::class)
->default(OverwriteAudio::NO->value),
Select::make(ReplaceRelatedAudio::getFieldKey())
->label(__('filament.actions.video.backfill.fields.replace_related.name'))
->helperText(__('filament.actions.video.backfill.fields.replace_related.help'))
->options(ReplaceRelatedAudio::asSelectArray())
->rules(['required', new Enum(ReplaceRelatedAudio::class)])
->required()
->enum(ReplaceRelatedAudio::class)
->default(ReplaceRelatedAudio::NO->value),
]);
}
@@ -49,7 +49,9 @@ trait MoveActionTrait
->label(__('filament.actions.storage.move.fields.path.name'))
->helperText(__('filament.actions.storage.move.fields.path.help'))
->required()
->rules(['required', 'string', 'doesnt_start_with:/', "ends_with:{$this->allowedFileExtension()}", new StorageFileDirectoryExistsRule($fs)])
->doesntStartWith('/')
->endsWith($this->allowedFileExtension())
->rule(new StorageFileDirectoryExistsRule($fs))
->default($defaultPath),
]);
}
@@ -64,7 +64,9 @@ trait MoveAllActionTrait
->label(__('filament.resources.singularLabel.video'))
->helperText(__('filament.actions.storage.move.fields.path.help'))
->required()
->rules(['required', 'string', 'doesnt_start_with:/', "ends_with:.webm", new StorageFileDirectoryExistsRule($videoFs)])
->doesntStartWith('/')
->endsWith('.webm')
->rule(new StorageFileDirectoryExistsRule($videoFs))
->default($videoPath),
TextInput::make('audio')
@@ -72,7 +74,9 @@ trait MoveAllActionTrait
->helperText(__('filament.actions.storage.move.fields.path.help'))
->hidden($audioPath === null)
->required($audioPath !== null)
->rules(['string', 'doesnt_start_with:/', "ends_with:.ogg", new StorageFileDirectoryExistsRule($audioFs)])
->doesntStartWith('/')
->endsWith('.ogg')
->rule(new StorageFileDirectoryExistsRule($audioFs))
->default($audioPath),
TextInput::make('script')
@@ -80,7 +84,9 @@ trait MoveAllActionTrait
->helperText(__('filament.actions.storage.move.fields.path.help'))
->hidden($scriptPath === null)
->required($scriptPath !== null)
->rules(['string', 'doesnt_start_with:/', "ends_with:.txt", new StorageFileDirectoryExistsRule($scriptFs)])
->doesntStartWith('/')
->endsWith('.txt')
->rule(new StorageFileDirectoryExistsRule($scriptFs))
->default($scriptPath),
]);
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Storage\Admin;
use App\Actions\Storage\Admin\Dump\DumpAction;
use App\Actions\Storage\Admin\Dump\DumpAdminAction;
/**
* Class AdminDumpCommand.
*/
class AdminDumpCommand extends DumpCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'db:dump-admin
{--comments : Write additional information in the MySQL dump such as program version, server version and host}
{--data-only : Dump only the data without the schema in PostgreSQL dump}
{--default-character-set=utf8 : Specify default character set in MySQL dump}
{--extended-insert : Use multiple-row insert syntax in MySQL dump}
{--inserts : Dump data as INSERT commands rather than COPY in PostgreSQL dump}
{--lock-tables : Lock all tables before dumping them to MySQL dump}
{--no-create-info : Turn off CREATE TABLE statements in MySQL dump}
{--quick : Retrieve rows for a table from the server one row at a time in MySQL dump}
{--set-gtid-purged=AUTO : Add SET GTID_PURGED to output in MySQL dump}
{--single-transaction : Issue a BEGIN SQL statement before dumping data from server for MySQL dump}
{--skip-column-statistics : Turn off ANALYZE table statements in the MySQL dump}
{--skip-comments : Do not write additional information in the MySQL dump}
{--skip-extended-insert : Turn off extended-insert in MySQL dump}
{--skip-lock-tables : Turn off locking tables before dumping to MySQL dump}
{--skip-quick : Do not retrieve rows for a table from the server one row at a time in MySQL dump}
';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Produces sanitized database dump, targeting admin-related tables for seeding purposes';
/**
* Get the underlying action.
*
* @return DumpAction
*/
protected function action(): DumpAction
{
return new DumpAdminAction($this->options());
}
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Storage\Admin;
use App\Actions\Storage\Admin\Dump\DumpAction;
use App\Actions\Storage\Admin\Dump\DumpAuthAction;
/**
* Class AuthDumpCommand.
*/
class AuthDumpCommand extends DumpCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'db:dump-auth
{--comments : Write additional information in the MySQL dump such as program version, server version and host}
{--data-only : Dump only the data without the schema in PostgreSQL dump}
{--default-character-set=utf8 : Specify default character set in MySQL dump}
{--extended-insert : Use multiple-row insert syntax in MySQL dump}
{--inserts : Dump data as INSERT commands rather than COPY in PostgreSQL dump}
{--lock-tables : Lock all tables before dumping them to MySQL dump}
{--no-create-info : Turn off CREATE TABLE statements in MySQL dump}
{--quick : Retrieve rows for a table from the server one row at a time in MySQL dump}
{--set-gtid-purged=AUTO : Add SET GTID_PURGED to output in MySQL dump}
{--single-transaction : Issue a BEGIN SQL statement before dumping data from server for MySQL dump}
{--skip-column-statistics : Turn off ANALYZE table statements in the MySQL dump}
{--skip-comments : Do not write additional information in the MySQL dump}
{--skip-extended-insert : Turn off extended-insert in MySQL dump}
{--skip-lock-tables : Turn off locking tables before dumping to MySQL dump}
{--skip-quick : Do not retrieve rows for a table from the server one row at a time in MySQL dump}
';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Produces sanitized database dump, targeting auth-related tables for seeding purposes';
/**
* Get the underlying action.
*
* @return DumpAction
*/
protected function action(): DumpAction
{
return new DumpAuthAction($this->options());
}
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Storage\Admin;
use App\Actions\Storage\Admin\Dump\DumpAction;
use App\Actions\Storage\Admin\Dump\DumpDiscordAction;
/**
* Class DiscordDumpCommand.
*/
class DiscordDumpCommand extends DumpCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'db:dump-discord
{--comments : Write additional information in the MySQL dump such as program version, server version and host}
{--data-only : Dump only the data without the schema in PostgreSQL dump}
{--default-character-set=utf8 : Specify default character set in MySQL dump}
{--extended-insert : Use multiple-row insert syntax in MySQL dump}
{--inserts : Dump data as INSERT commands rather than COPY in PostgreSQL dump}
{--lock-tables : Lock all tables before dumping them to MySQL dump}
{--no-create-info : Turn off CREATE TABLE statements in MySQL dump}
{--quick : Retrieve rows for a table from the server one row at a time in MySQL dump}
{--set-gtid-purged=AUTO : Add SET GTID_PURGED to output in MySQL dump}
{--single-transaction : Issue a BEGIN SQL statement before dumping data from server for MySQL dump}
{--skip-column-statistics : Turn off ANALYZE table statements in the MySQL dump}
{--skip-comments : Do not write additional information in the MySQL dump}
{--skip-extended-insert : Turn off extended-insert in MySQL dump}
{--skip-lock-tables : Turn off locking tables before dumping to MySQL dump}
{--skip-quick : Do not retrieve rows for a table from the server one row at a time in MySQL dump}
';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Produces sanitized database dump, targeting discord-related tables for seeding purposes';
/**
* Get the underlying action.
*
* @return DumpAction
*/
protected function action(): DumpAction
{
return new DumpDiscordAction($this->options());
}
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Storage\Admin;
use App\Actions\Storage\Admin\Dump\DumpAction;
use App\Actions\Storage\Admin\Dump\DumpListAction;
/**
* Class ListDumpCommand.
*/
class ListDumpCommand extends DumpCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'db:dump-list
{--comments : Write additional information in the MySQL dump such as program version, server version and host}
{--data-only : Dump only the data without the schema in PostgreSQL dump}
{--default-character-set=utf8 : Specify default character set in MySQL dump}
{--extended-insert : Use multiple-row insert syntax in MySQL dump}
{--inserts : Dump data as INSERT commands rather than COPY in PostgreSQL dump}
{--lock-tables : Lock all tables before dumping them to MySQL dump}
{--no-create-info : Turn off CREATE TABLE statements in MySQL dump}
{--quick : Retrieve rows for a table from the server one row at a time in MySQL dump}
{--set-gtid-purged=AUTO : Add SET GTID_PURGED to output in MySQL dump}
{--single-transaction : Issue a BEGIN SQL statement before dumping data from server for MySQL dump}
{--skip-column-statistics : Turn off ANALYZE table statements in the MySQL dump}
{--skip-comments : Do not write additional information in the MySQL dump}
{--skip-extended-insert : Turn off extended-insert in MySQL dump}
{--skip-lock-tables : Turn off locking tables before dumping to MySQL dump}
{--skip-quick : Do not retrieve rows for a table from the server one row at a time in MySQL dump}
';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Produces sanitized database dump, targeting list-related tables for seeding purposes';
/**
* Get the underlying action.
*
* @return DumpAction
*/
protected function action(): DumpAction
{
return new DumpListAction($this->options());
}
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Storage\Admin;
use App\Actions\Storage\Admin\Dump\DumpAction;
use App\Actions\Storage\Admin\Dump\DumpUserAction;
/**
* Class UserDumpCommand.
*/
class UserDumpCommand extends DumpCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'db:dump-user
{--comments : Write additional information in the MySQL dump such as program version, server version and host}
{--data-only : Dump only the data without the schema in PostgreSQL dump}
{--default-character-set=utf8 : Specify default character set in MySQL dump}
{--extended-insert : Use multiple-row insert syntax in MySQL dump}
{--inserts : Dump data as INSERT commands rather than COPY in PostgreSQL dump}
{--lock-tables : Lock all tables before dumping them to MySQL dump}
{--no-create-info : Turn off CREATE TABLE statements in MySQL dump}
{--quick : Retrieve rows for a table from the server one row at a time in MySQL dump}
{--set-gtid-purged=AUTO : Add SET GTID_PURGED to output in MySQL dump}
{--single-transaction : Issue a BEGIN SQL statement before dumping data from server for MySQL dump}
{--skip-column-statistics : Turn off ANALYZE table statements in the MySQL dump}
{--skip-comments : Do not write additional information in the MySQL dump}
{--skip-extended-insert : Turn off extended-insert in MySQL dump}
{--skip-lock-tables : Turn off locking tables before dumping to MySQL dump}
{--skip-quick : Do not retrieve rows for a table from the server one row at a time in MySQL dump}
';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Produces sanitized database dump, targeting user-related tables for seeding purposes';
/**
* Get the underlying action.
*
* @return DumpAction
*/
protected function action(): DumpAction
{
return new DumpUserAction($this->options());
}
}
+35
View File
@@ -5,8 +5,13 @@ declare(strict_types=1);
namespace App\Console;
use App\Console\Commands\Models\SyncViewAggregatesCommand;
use App\Console\Commands\Storage\Admin\AdminDumpCommand;
use App\Console\Commands\Storage\Admin\AuthDumpCommand;
use App\Console\Commands\Storage\Admin\DiscordDumpCommand;
use App\Console\Commands\Storage\Admin\DocumentDumpCommand;
use App\Console\Commands\Storage\Admin\DumpPruneCommand;
use App\Console\Commands\Storage\Admin\ListDumpCommand;
use App\Console\Commands\Storage\Admin\UserDumpCommand;
use App\Console\Commands\Storage\Admin\WikiDumpCommand;
use App\Models\BaseModel;
use BezhanSalleh\FilamentExceptions\Models\Exception;
@@ -44,12 +49,42 @@ class Kernel extends ConsoleKernel
->storeOutput()
->everyFifteenMinutes();
$schedule->command(AdminDumpCommand::class)
->withoutOverlapping()
->runInBackground()
->storeOutput()
->daily();
$schedule->command(AuthDumpCommand::class)
->withoutOverlapping()
->runInBackground()
->storeOutput()
->daily();
$schedule->command(DiscordDumpCommand::class)
->withoutOverlapping()
->runInBackground()
->storeOutput()
->daily();
$schedule->command(DocumentDumpCommand::class)
->withoutOverlapping()
->runInBackground()
->storeOutput()
->daily();
$schedule->command(ListDumpCommand::class)
->withoutOverlapping()
->runInBackground()
->storeOutput()
->daily();
$schedule->command(UserDumpCommand::class)
->withoutOverlapping()
->runInBackground()
->storeOutput()
->daily();
$schedule->command(WikiDumpCommand::class)
->withoutOverlapping()
->runInBackground()
+1 -1
View File
@@ -74,7 +74,7 @@ class ThemeCreated extends WikiCreatedEvent implements UpdateRelatedIndicesEvent
/**
* Update the sequence attribute of the first theme when creating a new sequence theme.
*
*
* @return void
*/
protected function updateFirstTheme(): void
@@ -42,7 +42,7 @@ class MembershipCreated extends WikiCreatedEvent implements UpdateRelatedIndices
*/
protected function getDiscordMessageDescription(): string
{
return "Membership: Member '**{$this->getModel()->member->getName()}**' of Group '**{$this->getModel()->artist->getName()}**' has been created.";
return "Membership '**{$this->getModel()->member->getName()}**' of Group '**{$this->getModel()->artist->getName()}**' has been created.";
}
/**
@@ -47,7 +47,7 @@ class MembershipDeleted extends WikiDeletedEvent implements UpdateRelatedIndices
*/
protected function getDiscordMessageDescription(): string
{
return "Membership: Member '**{$this->getModel()->member->getName()}**' of Group '**{$this->getModel()->artist->getName()}**' has been deleted.";
return "Membership '**{$this->getModel()->member->getName()}**' of Group '**{$this->getModel()->artist->getName()}**' has been deleted.";
}
/**
@@ -52,7 +52,7 @@ class PerformanceCreated extends WikiCreatedEvent implements UpdateRelatedIndice
$artist = $performance->artist;
if ($this->getModel()->isMembership()) {
return "Song '**{$song->getName()}**' has been attached to Artist '**{$artist->member->getName()}**' via Group '**{$artist->artist->getName()}**'.";
return "Song '**{$song->getName()}**' has been attached to Artist '**{$artist->member->getName()}**' as member of '**{$artist->artist->getName()}**'.";
}
return "Song '**{$song->getName()}**' has been attached to Artist '**{$artist->getName()}**'.";
@@ -14,6 +14,7 @@ use App\Models\Wiki\Song\Performance;
use App\Pivots\Wiki\ArtistSong;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
* Class PerformanceDeleted.
@@ -89,7 +90,14 @@ class PerformanceDeleted extends WikiDeletedEvent implements UpdateRelatedIndice
*/
public function updateRelatedIndices(): void
{
$performance = $this->getModel()->load([Performance::RELATION_ARTIST]);
$performance = $this->getModel()->load([
Performance::RELATION_ARTIST => function (MorphTo $morphTo) {
$morphTo->morphWith([
Artist::class => [],
Membership::class => [Membership::RELATION_ARTIST, Membership::RELATION_MEMBER]
]);
}
]);
if ($performance->isMembership()) {
$performance->artist->artist->searchable();
@@ -6,7 +6,10 @@ namespace App\Events\Wiki\Song\Performance;
use App\Contracts\Events\UpdateRelatedIndicesEvent;
use App\Events\BaseEvent;
use App\Models\Wiki\Artist;
use App\Models\Wiki\Song\Membership;
use App\Models\Wiki\Song\Performance;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
* Class PerformanceDeleting.
@@ -42,7 +45,14 @@ class PerformanceDeleting extends BaseEvent implements UpdateRelatedIndicesEvent
*/
public function updateRelatedIndices(): void
{
$performance = $this->getModel()->load([Performance::RELATION_ARTIST]);
$performance = $this->getModel()->load([
Performance::RELATION_ARTIST => function (MorphTo $morphTo) {
$morphTo->morphWith([
Artist::class => [],
Membership::class => [Membership::RELATION_ARTIST, Membership::RELATION_MEMBER]
]);
}
]);
if ($performance->isForceDeleting()) {
if ($performance->isMembership()) {
@@ -12,6 +12,7 @@ use App\Models\Wiki\Song\Membership;
use App\Models\Wiki\Song\Performance;
use App\Pivots\Wiki\ArtistSong;
use Exception;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
* Class PerformanceRestored.
@@ -57,7 +58,14 @@ class PerformanceRestored extends WikiRestoredEvent implements UpdateRelatedIndi
*/
public function updateRelatedIndices(): void
{
$performance = $this->getModel()->load([Performance::RELATION_ARTIST]);
$performance = $this->getModel()->load([
Performance::RELATION_ARTIST => function (MorphTo $morphTo) {
$morphTo->morphWith([
Artist::class => [],
Membership::class => [Membership::RELATION_ARTIST, Membership::RELATION_MEMBER]
]);
}
]);
if ($performance->isMembership()) {
$performance->artist->artist->searchable();
@@ -12,6 +12,7 @@ use App\Models\Wiki\Song\Membership;
use App\Models\Wiki\Song\Performance;
use App\Pivots\Wiki\ArtistSong;
use Exception;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
* Class PerformanceUpdated.
@@ -58,7 +59,14 @@ class PerformanceUpdated extends WikiUpdatedEvent implements UpdateRelatedIndice
*/
public function updateRelatedIndices(): void
{
$performance = $this->getModel()->load([Performance::RELATION_ARTIST]);
$performance = $this->getModel()->load([
Performance::RELATION_ARTIST => function (MorphTo $morphTo) {
$morphTo->morphWith([
Artist::class => [],
Membership::class => [Membership::RELATION_ARTIST, Membership::RELATION_MEMBER]
]);
}
]);
if ($performance->isMembership()) {
$performance->artist->artist->searchable();
@@ -13,7 +13,6 @@ use App\Models\Wiki\Video;
use Filament\Forms\Form;
use Filament\Support\Enums\MaxWidth;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Validation\Rules\Enum;
/**
* Class VideoDiscordNotificationBulkAction.
@@ -69,7 +68,7 @@ class VideoDiscordNotificationBulkAction extends BaseBulkAction
->options(DiscordNotificationType::asSelectArray())
->default(DiscordNotificationType::ADDED->value)
->required()
->rules(['required', new Enum(DiscordNotificationType::class)]),
->enum(DiscordNotificationType::class),
]);
}
}
+12 -4
View File
@@ -10,6 +10,7 @@ use App\Models\Auth\User;
use App\Models\BaseModel;
use Filament\Forms\Components\Select as ComponentsSelect;
use Filament\Forms\Form;
use Illuminate\Database\Eloquent\Builder;
use Laravel\Scout\Searchable;
/**
@@ -49,7 +50,7 @@ class BelongsTo extends ComponentsSelect
* Set the filament resource for the relation. Relation should be set if BelongsToThrough.
*
* @param class-string<BaseResource> $resource
* @param string|null $relation
* @param string|null $relation
* @return static
*/
public function resource(string $resource, ?string $relation = ''): static
@@ -65,9 +66,10 @@ class BelongsTo extends ComponentsSelect
* Determine if the create option is available. The resource is required for this.
*
* @param bool $condition
* @param array|null $eagerLoads
* @return static
*/
public function showCreateOption(bool $condition = true): static
public function showCreateOption(bool $condition = true, ?array $eagerLoads = []): static
{
$this->showCreateOption = $condition;
$this->reload();
@@ -87,12 +89,17 @@ class BelongsTo extends ComponentsSelect
$this->searchable();
$this->getOptionLabelUsing(fn ($state) => static::getSearchLabelWithBlade($model::find($state)));
$eagerLoads = method_exists($model, 'getEagerLoadsForSubtitle')
? $model::getEagerLoadsForSubtitle()
: [];
if (in_array(Searchable::class, class_uses_recursive($model))) {
return $this
->getSearchResultsUsing(function (string $search) use ($model) {
->getSearchResultsUsing(function (string $search) use ($model, $eagerLoads) {
$search = $this->escapeReservedChars($search);
/** @phpstan-ignore-next-line */
return $model::search($search)
->query(fn (Builder $query) => $query->with($eagerLoads))
->take(25)
->get()
->mapWithKeys(fn (BaseModel $model) => [$model->getKey() => static::getSearchLabelWithBlade($model)])
@@ -101,9 +108,10 @@ class BelongsTo extends ComponentsSelect
}
return $this
->getSearchResultsUsing(function (string $search) use ($model) {
->getSearchResultsUsing(function (string $search) use ($model, $eagerLoads) {
return $model::query()
->where($this->resource->getRecordTitleAttribute(), ComparisonOperator::LIKE->value, "%$search%")
->with($eagerLoads)
->take(25)
->get()
->mapWithKeys(fn (BaseModel|User $model) => [$model->getKey() => static::getSearchLabelWithBlade($model)])
+15 -2
View File
@@ -10,6 +10,7 @@ use App\Filament\Components\Columns\BelongsToColumn;
use App\Filament\Components\Columns\TextColumn;
use App\Filament\Components\Filters\DateFilter;
use App\Filament\Components\Infolist\BelongsToEntry;
use App\Filament\Components\Infolist\KeyValueThreeEntry;
use App\Filament\Components\Infolist\TextEntry;
use App\Filament\Resources\Admin\ActionLog\Pages\ManageActionLogs;
use App\Filament\Resources\Auth\User;
@@ -18,9 +19,11 @@ use App\Models\Admin\ActionLog as ActionLogModel;
use App\Models\Auth\User as UserModel;
use App\Models\BaseModel;
use Filament\Facades\Filament;
use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Form;
use Filament\Infolists\Components\KeyValueEntry;
use Filament\Infolists\Components\RepeatableEntry;
use Filament\Infolists\Components\TextEntry\TextEntrySize;
use Filament\Infolists\Infolist;
use Filament\Tables\Filters\SelectFilter;
@@ -139,6 +142,13 @@ class ActionLog extends BaseResource
{
return $form
->schema([
KeyValue::make(ActionLogModel::ATTRIBUTE_FIELDS)
->label(__('filament.fields.action_log.fields.name'))
->keyLabel(__('filament.fields.action_log.fields.keys'))
->valueLabel(__('filament.fields.action_log.fields.values'))
->columnSpanFull()
->hidden(fn ($state) => is_null($state)),
Textarea::make(ActionLogModel::ATTRIBUTE_EXCEPTION)
->label(__('filament.fields.action_log.exception'))
->disabled()
@@ -221,7 +231,9 @@ class ActionLog extends BaseResource
->dateTime(),
KeyValueEntry::make(ActionLogModel::ATTRIBUTE_FIELDS)
->label(__('filament.fields.action_log.fields'))
->label(__('filament.fields.action_log.fields.name'))
->keyLabel(__('filament.fields.action_log.fields.keys'))
->valueLabel(__('filament.fields.action_log.fields.values'))
->columnSpanFull()
->hidden(fn ($state) => is_null($state)),
@@ -229,7 +241,8 @@ class ActionLog extends BaseResource
->label(__('filament.fields.action_log.exception'))
->columnSpanFull()
->size(TextEntrySize::Large),
])->columns(3);
])
->columns(3);
}
/**
@@ -101,7 +101,6 @@ class Announcement extends BaseResource
MarkdownEditor::make(AnnouncementModel::ATTRIBUTE_CONTENT)
->label(__('filament.fields.announcement.content'))
->required()
->rules(['required', 'max:65535'])
->maxLength(65535),
])
->columns(1);
-1
View File
@@ -119,7 +119,6 @@ class Dump extends BaseResource
->label(__('filament.fields.dump.path'))
->required()
->maxLength(192)
->rules(['required', 'max:192'])
->hiddenOn(['create', 'edit']),
])
->columns(1);
+2 -4
View File
@@ -117,15 +117,13 @@ class Feature extends BaseResource
->helperText(__('filament.fields.feature.key.help'))
->readOnly()
->required()
->maxLength(192)
->rules(['required', 'max:192']),
->maxLength(192),
TextInput::make(FeatureModel::ATTRIBUTE_VALUE)
->label(__('filament.fields.feature.value.name'))
->helperText(__('filament.fields.feature.value.help'))
->required()
->maxLength(192)
->rules(['required', 'max:192']),
->maxLength(192),
])
->columns(1);
}
@@ -129,8 +129,6 @@ class FeaturedTheme extends BaseResource
*/
public static function form(Form $form): Form
{
$allowedDateFormats = array_column(AllowedDateFormat::cases(), 'value');
return $form
->schema([
DatePicker::make(FeaturedThemeModel::ATTRIBUTE_START_AT)
@@ -121,7 +121,6 @@ class Permission extends BaseResource
->label(__('filament.fields.permission.name'))
->required()
->maxLength(192)
->rules(['required', 'max:192'])
->hiddenOn(['edit']),
])
->columns(1);
+4 -7
View File
@@ -128,13 +128,12 @@ class Role extends BaseResource
TextInput::make(RoleModel::ATTRIBUTE_NAME)
->label(__('filament.fields.role.name'))
->required()
->maxLength(192)
->rules(['required', 'max:192']),
->maxLength(192),
Checkbox::make(RoleModel::ATTRIBUTE_DEFAULT)
->label(__('filament.fields.role.default.name'))
->helperText(__('filament.fields.role.default.help'))
->rules(['nullable', 'boolean']),
->rules(['boolean']),
ColorPicker::make(RoleModel::ATTRIBUTE_COLOR)
->label(__('filament.fields.role.color.name'))
@@ -143,10 +142,8 @@ class Role extends BaseResource
TextInput::make(RoleModel::ATTRIBUTE_PRIORITY)
->label(__('filament.fields.role.priority.name'))
->helperText(__('filament.fields.role.priority.help'))
->numeric()
->minValue(1)
->nullable()
->rules(['nullable', 'integer', 'min:1']),
->integer()
->minValue(1),
])
->columns(1);
}
+2 -4
View File
@@ -125,15 +125,13 @@ class User extends BaseResource
TextInput::make(UserModel::ATTRIBUTE_NAME)
->label(__('filament.fields.user.name'))
->required()
->maxLength(192)
->rules(['required', 'max:192']),
->maxLength(192),
TextInput::make(UserModel::ATTRIBUTE_EMAIL)
->label(__('filament.fields.user.email'))
->email()
->required()
->maxLength(192)
->rules(['required', 'email', 'max:192']),
->maxLength(192),
])
->columns(1);
}
@@ -143,15 +143,13 @@ class DiscordThread extends BaseResource
->disabledOn(['edit'])
->formatStateUsing(fn ($state) => strval($state))
->required()
->rules(['required'])
->live()
->afterStateUpdated(fn (Set $set, string $state) => $set(DiscordThreadModel::ATTRIBUTE_NAME, Arr::get(new DiscordThreadAction()->get($state), 'thread.name'))),
TextInput::make(DiscordThreadModel::ATTRIBUTE_NAME)
->label(__('filament.fields.discord_thread.name.name'))
->helperText(__('filament.fields.discord_thread.name.help'))
->required()
->rules(['required', 'string']),
->required(),
BelongsTo::make(DiscordThreadModel::ATTRIBUTE_ANIME)
->resource(AnimeResource::class)
-2
View File
@@ -121,7 +121,6 @@ class Page extends BaseResource
->helperText(__('filament.fields.page.name.help'))
->required()
->maxLength(192)
->rules(['required', 'max:192'])
->live(true)
->afterStateUpdated(fn (Set $set, Get $get) => static::setSlug($set, $get)),
@@ -135,7 +134,6 @@ class Page extends BaseResource
->helperText(__('filament.fields.page.body.help'))
->required()
->maxLength(16777215)
->rules(['required', 'max:16777215'])
->columnSpan(2)
->formatStateUsing(fn (?PageModel $record) => $record?->body),
])
+3 -5
View File
@@ -29,7 +29,6 @@ use Filament\Resources\RelationManagers\RelationGroup;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Validation\Rules\Enum;
/**
* Class ExternalEntry.
@@ -132,13 +131,11 @@ class ExternalEntry extends BaseResource
BelongsTo::make(ExternalEntryModel::ATTRIBUTE_PROFILE)
->resource(ExternalProfileResource::class)
->required()
->rules(['required'])
->hiddenOn([ExternalEntryExternalProfileRelationManager::class]),
BelongsTo::make(ExternalEntryModel::ATTRIBUTE_ANIME)
->resource(Anime::class)
->required()
->rules(['required']),
->required(),
TextInput::make(ExternalEntryModel::ATTRIBUTE_SCORE)
->label(__('filament.fields.external_entry.score.name'))
@@ -149,7 +146,8 @@ class ExternalEntry extends BaseResource
->label(__('filament.fields.external_entry.watch_status.name'))
->helperText(__('filament.fields.external_entry.watch_status.help'))
->options(ExternalEntryWatchStatus::asSelectArray())
->rules([new Enum(ExternalEntryWatchStatus::class)]),
->required()
->enum(ExternalEntryWatchStatus::class),
Checkbox::make(ExternalEntryModel::ATTRIBUTE_IS_FAVORITE)
->label(__('filament.fields.external_entry.is_favorite.name'))
@@ -26,7 +26,6 @@ use Filament\Infolists\Infolist;
use Filament\Resources\RelationManagers\RelationGroup;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Validation\Rules\Enum;
/**
* Class ExternalProfile.
@@ -142,22 +141,21 @@ class ExternalProfile extends BaseResource
->label(__('filament.fields.external_profile.name.name'))
->helperText(__('filament.fields.external_profile.name.help'))
->required()
->maxLength(192)
->rules(['required', 'max:192']),
->maxLength(192),
Select::make(ExternalProfileModel::ATTRIBUTE_SITE)
->label(__('filament.fields.external_profile.site.name'))
->helperText(__('filament.fields.external_profile.site.help'))
->options(ExternalProfileSite::asSelectArray())
->required()
->rules(['required', new Enum(ExternalProfileSite::class)]),
->enum(ExternalProfileSite::class),
Select::make(ExternalProfileModel::ATTRIBUTE_VISIBILITY)
->label(__('filament.fields.external_profile.visibility.name'))
->helperText(__('filament.fields.external_profile.visibility.help'))
->options(ExternalProfileVisibility::asSelectArray())
->required()
->rules(['required', new Enum(ExternalProfileVisibility::class)]),
->enum(ExternalProfileVisibility::class),
])
->columns(2);
}
+2 -6
View File
@@ -29,7 +29,6 @@ use Filament\Infolists\Infolist;
use Filament\Resources\RelationManagers\RelationGroup;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Validation\Rules\Enum;
/**
* Class Playlist.
@@ -149,15 +148,14 @@ class Playlist extends BaseResource
->label(__('filament.fields.playlist.name.name'))
->helperText(__('filament.fields.playlist.name.help'))
->required()
->maxLength(192)
->rules(['required', 'max:192']),
->maxLength(192),
Select::make(PlaylistModel::ATTRIBUTE_VISIBILITY)
->label(__('filament.fields.playlist.visibility.name'))
->helperText(__('filament.fields.playlist.visibility.help'))
->options(PlaylistVisibility::asSelectArray())
->required()
->rules(['required', new Enum(PlaylistVisibility::class)]),
->enum(PlaylistVisibility::class),
TextInput::make(PlaylistModel::ATTRIBUTE_HASHID)
->label(__('filament.fields.playlist.hashid.name'))
@@ -175,9 +173,7 @@ class Playlist extends BaseResource
Textarea::make(PlaylistModel::ATTRIBUTE_DESCRIPTION)
->label(__('filament.fields.playlist.description.name'))
->helperText(__('filament.fields.playlist.description.help'))
->nullable()
->maxLength(1000)
->rules(['nullable', 'max:1000'])
->columnSpanFull(),
])
->columns(2);
@@ -149,7 +149,6 @@ class Track extends BaseResource
BelongsTo::make(TrackModel::ATTRIBUTE_PLAYLIST)
->resource(PlaylistResource::class)
->required()
->rules(['required'])
->hiddenOn([TrackPlaylistRelationManager::class]),
BelongsTo::make(TrackModel::ATTRIBUTE_ENTRY)
+7 -10
View File
@@ -39,7 +39,6 @@ use Filament\Tables\Actions\ActionGroup;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Enum;
/**
* Class Anime.
@@ -152,7 +151,6 @@ class Anime extends BaseResource
->helperText(__('filament.fields.anime.name.help'))
->required()
->maxLength(255)
->rules(['required', 'max:255'])
->live(true)
->afterStateUpdated(fn (Set $set, ?string $state) => $set(AnimeModel::ATTRIBUTE_SLUG, Str::slug($state, '_'))),
@@ -163,9 +161,9 @@ class Anime extends BaseResource
TextInput::make(AnimeModel::ATTRIBUTE_YEAR)
->label(__('filament.fields.anime.year.name'))
->helperText(__('filament.fields.anime.year.help'))
->numeric()
->required()
->rules(['required', 'digits:4', 'integer'])
->integer()
->length(4)
->minValue(1960)
->maxValue(intval(date('Y')) + 1),
@@ -173,24 +171,23 @@ class Anime extends BaseResource
->label(__('filament.fields.anime.season.name'))
->helperText(__('filament.fields.anime.season.help'))
->options(AnimeSeason::asSelectArrayStyled())
->searchable()
->allowHtml()
->required()
->rules(['required', new Enum(AnimeSeason::class)]),
->enum(AnimeSeason::class)
->searchable()
->allowHtml(),
Select::make(AnimeModel::ATTRIBUTE_MEDIA_FORMAT)
->label(__('filament.fields.anime.media_format.name'))
->helperText(__('filament.fields.anime.media_format.help'))
->options(AnimeMediaFormat::asSelectArray())
->required()
->rules(['required', new Enum(AnimeMediaFormat::class)]),
->enum(AnimeMediaFormat::class),
MarkdownEditor::make(AnimeModel::ATTRIBUTE_SYNOPSIS)
->label(__('filament.fields.anime.synopsis.name'))
->helperText(__('filament.fields.anime.synopsis.help'))
->columnSpan(2)
->maxLength(65535)
->rules('max:65535'),
->maxLength(65535),
])
->columns(2);
}
@@ -28,7 +28,6 @@ use Filament\Resources\RelationManagers\RelationGroup;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Validation\Rules\Enum;
/**
* Class Synonym.
@@ -146,14 +145,13 @@ class Synonym extends BaseResource
->helperText(__('filament.fields.anime_synonym.type.help'))
->options(AnimeSynonymType::asSelectArray())
->required()
->rules(['required', new Enum(AnimeSynonymType::class)]),
->enum(AnimeSynonymType::class),
TextInput::make(SynonymModel::ATTRIBUTE_TEXT)
->label(__('filament.fields.anime_synonym.text.name'))
->helperText(__('filament.fields.anime_synonym.text.help'))
->required()
->maxLength(255)
->rules(['required', 'max:255']),
->maxLength(255),
])
->columns(1);
}
+25 -19
View File
@@ -29,8 +29,10 @@ use App\Filament\Resources\Wiki\Song\RelationManagers\PerformanceSongRelationMan
use App\Filament\Resources\Wiki\Song\RelationManagers\ThemeSongRelationManager;
use App\Models\Wiki\Anime\AnimeTheme as ThemeModel;
use App\Models\Wiki\Anime\AnimeTheme;
use App\Models\Wiki\Artist;
use App\Models\Wiki\Group;
use App\Models\Wiki\Song;
use App\Models\Wiki\Song\Membership;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
@@ -47,8 +49,8 @@ use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Enum;
/**
* Class Theme.
@@ -120,7 +122,7 @@ class Theme extends BaseResource
*/
public static function getRecordTitle(?Model $record): ?string
{
return $record instanceof ThemeModel ? $record->anime->getName() . ' ' . $record->slug : null;
return $record instanceof ThemeModel ? $record->getName() : null;
}
/**
@@ -155,10 +157,20 @@ class Theme extends BaseResource
$query = parent::getEloquentQuery();
// Necessary to prevent lazy loading when loading related resources
/** @phpstan-ignore-next-line */
return $query->with([
AnimeTheme::RELATION_ANIME,
AnimeTheme::RELATION_GROUP,
AnimeTheme::RELATION_ENTRIES,
AnimeTheme::RELATION_PERFORMANCES,
AnimeTheme::RELATION_SONG,
AnimeTheme::RELATION_GROUP
'song.animethemes',
AnimeTheme::RELATION_PERFORMANCES_ARTISTS => function (MorphTo $morphTo) {
$morphTo->morphWith([
Artist::class => [],
Membership::class => [Membership::RELATION_ARTIST, Membership::RELATION_MEMBER]
]);
},
]);
}
@@ -182,33 +194,31 @@ class Theme extends BaseResource
BelongsTo::make(ThemeModel::ATTRIBUTE_ANIME)
->resource(AnimeResource::class)
->hiddenOn(ThemeRelationManager::class)
->required()
->rules(['required']),
->required(),
Select::make(ThemeModel::ATTRIBUTE_TYPE)
->label(__('filament.fields.anime_theme.type.name'))
->helperText(__('filament.fields.anime_theme.type.help'))
->options(ThemeType::asSelectArray())
->required()
->enum(ThemeType::class)
->live(true)
->afterStateUpdated(fn (Set $set, Get $get) => Theme::setThemeSlug($set, $get))
->rules(['required', new Enum(ThemeType::class)]),
->afterStateUpdated(fn (Set $set, Get $get) => Theme::setThemeSlug($set, $get)),
TextInput::make(ThemeModel::ATTRIBUTE_SEQUENCE)
->label(__('filament.fields.anime_theme.sequence.name'))
->helperText(__('filament.fields.anime_theme.sequence.help'))
->numeric()
->integer()
->live(true)
->afterStateUpdated(fn (Set $set, Get $get) => Theme::setThemeSlug($set, $get))
->rules(['nullable', 'integer']),
->afterStateUpdated(fn (Set $set, Get $get) => Theme::setThemeSlug($set, $get)),
TextInput::make(ThemeModel::ATTRIBUTE_SLUG)
->label(__('filament.fields.anime_theme.slug.name'))
->helperText(__('filament.fields.anime_theme.slug.help'))
->required()
->readOnly()
->maxLength(192)
->rules(['required', 'max:192', 'alpha_dash']),
->alphaDash()
->readOnly(),
BelongsTo::make(ThemeModel::ATTRIBUTE_GROUP)
->resource(GroupResource::class)
@@ -227,8 +237,6 @@ class Theme extends BaseResource
->afterStateUpdated(function (Set $set, $state) {
/** @var Song|null $song */
$song = Song::find($state);
if (!$song) return;
$set('performances', PerformanceSongRelationManager::formatArtists($song));
}),
@@ -273,8 +281,7 @@ class Theme extends BaseResource
->label(__('filament.fields.anime_theme.sequence.name')),
TextColumn::make(ThemeModel::ATTRIBUTE_SLUG)
->label(__('filament.fields.anime_theme.slug.name'))
->formatStateUsing(fn ($record) => $record->getName()),
->label(__('filament.fields.anime_theme.slug.name')),
BelongsToColumn::make(ThemeModel::RELATION_GROUP, GroupResource::class),
@@ -310,9 +317,8 @@ class Theme extends BaseResource
TextEntry::make(ThemeModel::ATTRIBUTE_SEQUENCE)
->label(__('filament.fields.anime_theme.sequence.name')),
TextEntry::make(ThemeModel::ATTRIBUTE_ID)
->label(__('filament.fields.anime_theme.slug.name'))
->formatStateUsing(fn ($state) => ThemeModel::withTrashed()->find(intval($state))->getName()),
TextEntry::make(ThemeModel::ATTRIBUTE_SLUG)
->label(__('filament.fields.anime_theme.slug.name')),
BelongsToEntry::make(ThemeModel::RELATION_GROUP, GroupResource::class)
->label(__('filament.resources.singularLabel.group')),
@@ -169,7 +169,6 @@ class Entry extends BaseResource
->resource(AnimeResource::class, EntryModel::RELATION_ANIME_SHALLOW)
->live(true)
->required()
->rules(['required'])
->visibleOn([ListEntries::class, ViewEntry::class])
->saveRelationshipsUsing(fn (EntryModel $record, $state) => $record->animetheme->anime()->associate(intval($state))->save()),
@@ -177,7 +176,6 @@ class Entry extends BaseResource
->label(__('filament.resources.singularLabel.anime_theme'))
->relationship(EntryModel::RELATION_THEME, ThemeModel::ATTRIBUTE_ID)
->required()
->rules(['required'])
->visibleOn([ListEntries::class, ViewEntry::class])
->options(function (Get $get) {
return ThemeModel::query()
@@ -190,30 +188,27 @@ class Entry extends BaseResource
TextInput::make(EntryModel::ATTRIBUTE_VERSION)
->label(__('filament.fields.anime_theme_entry.version.name'))
->helperText(__('filament.fields.anime_theme_entry.version.help'))
->numeric()
->rules(['nullable', 'integer']),
->integer(),
TextInput::make(EntryModel::ATTRIBUTE_EPISODES)
->label(__('filament.fields.anime_theme_entry.episodes.name'))
->helperText(__('filament.fields.anime_theme_entry.episodes.help'))
->maxLength(192)
->rules(['nullable', 'max:192']),
->maxLength(192),
Checkbox::make(EntryModel::ATTRIBUTE_NSFW)
->label(__('filament.fields.anime_theme_entry.nsfw.name'))
->helperText(__('filament.fields.anime_theme_entry.nsfw.help'))
->rules(['nullable', 'boolean']),
->rules(['boolean']),
Checkbox::make(EntryModel::ATTRIBUTE_SPOILER)
->label(__('filament.fields.anime_theme_entry.spoiler.name'))
->helperText(__('filament.fields.anime_theme_entry.spoiler.help'))
->rules(['nullable', 'boolean']),
->rules(['boolean']),
TextInput::make(EntryModel::ATTRIBUTE_NOTES)
->label(__('filament.fields.anime_theme_entry.notes.name'))
->helperText(__('filament.fields.anime_theme_entry.notes.help'))
->maxLength(192)
->rules(['nullable', 'max:192']),
->maxLength(192),
])
->columns(1);
}
+1 -3
View File
@@ -145,7 +145,6 @@ class Artist extends BaseResource
->label(__('filament.fields.artist.name.name'))
->helperText(__('filament.fields.artist.name.help'))
->required()
->rules(['required', 'max:192'])
->maxLength(192)
->live(true)
->afterStateUpdated(fn (Set $set, ?string $state) => $set(ArtistModel::ATTRIBUTE_SLUG, Str::slug($state, '_'))),
@@ -158,8 +157,7 @@ class Artist extends BaseResource
->label(__('filament.fields.artist.information.name'))
->helperText(__('filament.fields.artist.information.help'))
->columnSpan(2)
->maxLength(65535)
->rules('max:65535'),
->maxLength(65535),
])
->columns(2);
}
@@ -28,7 +28,6 @@ use Filament\Infolists\Infolist;
use Filament\Resources\RelationManagers\RelationGroup;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Validation\Rules\Enum;
/**
* Class ExternalResource.
@@ -129,7 +128,7 @@ class ExternalResource extends BaseResource
->helperText(__('filament.fields.external_resource.site.help'))
->options(ResourceSite::asSelectArray())
->required()
->rules(['required', new Enum(ResourceSite::class)]),
->enum(ResourceSite::class),
TextInput::make(ExternalResourceModel::ATTRIBUTE_LINK)
->label(__('filament.fields.external_resource.link.name'))
@@ -146,8 +145,7 @@ class ExternalResource extends BaseResource
TextInput::make(ExternalResourceModel::ATTRIBUTE_EXTERNAL_ID)
->label(__('filament.fields.external_resource.external_id.name'))
->helperText(__('filament.fields.external_resource.external_id.help'))
->numeric()
->rules(['nullable', 'integer']),
->integer(),
])
->columns(1);
}
+1 -2
View File
@@ -117,8 +117,7 @@ class Group extends BaseResource
->label(__('filament.fields.group.name.name'))
->helperText(__('filament.fields.group.name.help'))
->required()
->maxLength(192)
->rules(['required', 'max:192']),
->maxLength(192),
TextInput::make(GroupModel::ATTRIBUTE_SLUG)
->label(__('filament.fields.group.slug.name'))
+1 -2
View File
@@ -28,7 +28,6 @@ use Filament\Tables\Columns\Layout\Stack;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Support\Facades\Config;
use Illuminate\Validation\Rules\Enum;
/**
* Class Image.
@@ -129,7 +128,7 @@ class Image extends BaseResource
->helperText(__('filament.fields.image.facet.help'))
->options(ImageFacet::asSelectArray())
->required()
->rules(['required', new Enum(ImageFacet::class)]),
->enum(ImageFacet::class),
]);
}
-1
View File
@@ -133,7 +133,6 @@ class Series extends BaseResource
->helperText(__('filament.fields.series.name.help'))
->required()
->maxLength(192)
->rules(['required', 'max:192'])
->live(true)
->afterStateUpdated(fn (Set $set, ?string $state) => $set(SeriesModel::ATTRIBUTE_SLUG, Str::slug($state, '_'))),
+1 -2
View File
@@ -136,8 +136,7 @@ class Song extends BaseResource
->label(__('filament.fields.song.title.name'))
->helperText(__('filament.fields.song.title.help'))
->required()
->maxLength(192)
->rules(['required', 'max:192']),
->maxLength(192),
]);
}
@@ -367,18 +367,12 @@ class Performance extends BaseResource
->defaultItems(0)
->columns(3)
->columnSpanFull()
->formatStateUsing(function ($livewire, Get $get, $state) {
if ($livewire instanceof PerformanceSongRelationManager) {
$song = $livewire->getOwnerRecord();
} else {
if ($songId = $get(PerformanceModel::ATTRIBUTE_SONG)) {
$song = SongModel::find($songId);
} else return $state;
}
->formatStateUsing(function ($livewire, Get $get) {
/** @var SongModel|null $song */
$song = $livewire instanceof PerformanceSongRelationManager
? $livewire->getOwnerRecord()
: SongModel::find($get(PerformanceModel::ATTRIBUTE_SONG));
if (!($song instanceof SongModel)) {
return $state;
}
return PerformanceSongRelationManager::formatArtists($song);
})
->schema([
@@ -386,13 +380,20 @@ class Performance extends BaseResource
->resource(ArtistResource::class)
->showCreateOption()
->required()
->rules(['required'])
->hintAction(
Action::make('load')
->label(__('filament.fields.performance.load_members.name'))
->action(function (Get $get, Set $set) {
$artistId = $get(Artist::ATTRIBUTE_ID);
if ($artistId === null) {
$set('memberships', []);
return;
}
/** @var Artist $group */
$group = Artist::query()->find($get(Artist::ATTRIBUTE_ID));
$group = Artist::query()
->with([Artist::RELATION_MEMBERS])
->find($artistId);
$set('memberships', $group->members->map(fn (Artist $member) => [
Membership::ATTRIBUTE_MEMBER => $member->getKey(),
@@ -423,8 +424,7 @@ class Performance extends BaseResource
->resource(ArtistResource::class)
->showCreateOption()
->label(__('filament.fields.membership.member'))
->required()
->rules(['required']),
->required(),
TextInput::make(Membership::ATTRIBUTE_AS)
->label(__('filament.fields.membership.as.name'))
@@ -91,7 +91,7 @@ class PerformanceSongRelationManager extends PerformanceRelationManager
return [
Action::make('manage performances')
->label(__('filament.actions.performances.manage_performances'))
->action(fn ($livewire, $data) => static::saveArtists($livewire->getOwnerRecord(), $data[Song::RELATION_PERFORMANCES]))
->action(fn ($livewire, $data) => static::saveArtists($livewire->getOwnerRecord(), Arr::get($data, Song::RELATION_PERFORMANCES)))
->form(PerformanceResource::performancesFields()),
];
}
@@ -99,11 +99,17 @@ class PerformanceSongRelationManager extends PerformanceRelationManager
/**
* Format artists to the action.
*
* @param Song $song
* @param Song|null $song
* @return array
*/
public static function formatArtists(Song $song): array
public static function formatArtists(?Song $song = null): array
{
if (!($song instanceof Song)) {
return [];
}
$song->load(Song::RELATION_PERFORMANCE_ARTISTS);
$performances = $song->performances;
$artists = [];
@@ -145,12 +151,16 @@ class PerformanceSongRelationManager extends PerformanceRelationManager
/**
* Save the artists to the action.
*
* @param Song $song
* @param array $data
* @param Song|null $song
* @param array|null $data
* @return void
*/
public static function saveArtists(Song $song, array $data): void
public static function saveArtists(?Song $song = null, ?array $data = []): void
{
if (!($song instanceof Song) || empty($data)) {
return;
}
$action = new ManageSongPerformances();
$action->forSong($song);
-1
View File
@@ -128,7 +128,6 @@ class Studio extends BaseResource
->helperText(__('filament.fields.studio.name.help'))
->required()
->maxLength(192)
->rules(['required', 'max:192'])
->live(true)
->afterStateUpdated(fn (Set $set, ?string $state) => $set(StudioModel::ATTRIBUTE_SLUG, Str::slug($state, '_'))),
+8 -11
View File
@@ -40,7 +40,6 @@ use Filament\Tables\Actions\ActionGroup;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Validation\Rules\Enum;
/**
* Class Video.
@@ -139,44 +138,42 @@ class Video extends BaseResource
TextInput::make(VideoModel::ATTRIBUTE_RESOLUTION)
->label(__('filament.fields.video.resolution.name'))
->helperText(__('filament.fields.video.resolution.help'))
->numeric()
->integer()
->minValue(360)
->maxValue(1080)
->nullable()
->rules(['nullable', 'integer']),
->maxValue(1080),
Checkbox::make(VideoModel::ATTRIBUTE_NC)
->label(__('filament.fields.video.nc.name'))
->helperText(__('filament.fields.video.nc.help'))
->rules(['nullable', 'boolean']),
->rules(['boolean']),
Checkbox::make(VideoModel::ATTRIBUTE_SUBBED)
->label(__('filament.fields.video.subbed.name'))
->helperText(__('filament.fields.video.subbed.help'))
->rules(['nullable', 'boolean']),
->rules(['boolean']),
Checkbox::make(VideoModel::ATTRIBUTE_LYRICS)
->label(__('filament.fields.video.lyrics.name'))
->helperText(__('filament.fields.video.lyrics.help'))
->rules(['nullable', 'boolean']),
->rules(['boolean']),
Checkbox::make(VideoModel::ATTRIBUTE_UNCEN)
->label(__('filament.fields.video.uncen.name'))
->helperText(__('filament.fields.video.uncen.help'))
->rules(['nullable', 'boolean']),
->rules(['boolean']),
Select::make(VideoModel::ATTRIBUTE_OVERLAP)
->label(__('filament.fields.video.overlap.name'))
->helperText(__('filament.fields.video.overlap.help'))
->options(VideoOverlap::asSelectArray())
->rules(['nullable', new Enum(VideoOverlap::class)]),
->enum(VideoOverlap::class),
Select::make(VideoModel::ATTRIBUTE_SOURCE)
->label(__('filament.fields.video.source.name'))
->helperText(__('filament.fields.video.source.help'))
->options(VideoSource::asSelectArray())
->required()
->rules(['required', new Enum(VideoSource::class)]),
->enum(VideoSource::class),
Select::make(VideoModel::ATTRIBUTE_AUDIO)
->label(__('filament.resources.singularLabel.audio'))
@@ -70,7 +70,6 @@ class DiscordEditMessageTableAction extends BaseTableAction
->required()
->autofocus()
->regex('/https:\/\/discord\.com\/channels\/\d+\/\d+\/\d+/')
->rules(['required', 'string', 'regex:/https:\/\/discord\.com\/channels\/\d+\/\d+\/\d+/'])
->hintAction(
Action::make('load')
->label(__('filament.table_actions.discord_thread.message.url.action'))
@@ -125,8 +124,7 @@ class DiscordEditMessageTableAction extends BaseTableAction
RichEditor::make(DiscordEmbed::ATTRIBUTE_DESCRIPTION)
->label(__('filament.table_actions.discord_thread.message.embeds.body.description.name'))
->helperText(__('filament.table_actions.discord_thread.message.embeds.body.description.help'))
->required()
->rules(['required', 'string']),
->required(),
ColorPicker::make(DiscordEmbed::ATTRIBUTE_COLOR)
->label(__('filament.table_actions.discord_thread.message.embeds.body.color.name'))
@@ -149,14 +147,12 @@ class DiscordEditMessageTableAction extends BaseTableAction
TextInput::make(DiscordEmbed::ATTRIBUTE_FIELDS_NAME)
->label(__('filament.table_actions.discord_thread.message.embeds.body.fields.name.name'))
->helperText(__('filament.table_actions.discord_thread.message.embeds.body.fields.name.help'))
->required()
->rules(['required', 'string']),
->required(),
TextInput::make(DiscordEmbed::ATTRIBUTE_FIELDS_VALUE)
->label(__('filament.table_actions.discord_thread.message.embeds.body.fields.value.name'))
->helperText(__('filament.table_actions.discord_thread.message.embeds.body.fields.value.help'))
->required()
->rules(['required', 'string']),
->required(),
Checkbox::make(DiscordEmbed::ATTRIBUTE_FIELDS_INLINE)
->label(__('filament.table_actions.discord_thread.message.embeds.body.fields.inline.name'))
@@ -175,8 +171,7 @@ class DiscordEditMessageTableAction extends BaseTableAction
TextInput::make(DiscordMessage::ATTRIBUTE_URL)
->label(__('filament.table_actions.discord_thread.message.images.body.url.name'))
->helperText(__('filament.table_actions.discord_thread.message.images.body.url.help'))
->required()
->rules(['required', 'string']),
->required(),
]),
]);
}
@@ -64,8 +64,7 @@ class DiscordSendMessageTableAction extends BaseTableAction
TextInput::make(DiscordMessage::ATTRIBUTE_CHANNEL_ID)
->label(__('filament.table_actions.discord_thread.message.channelId.name'))
->helperText(__('filament.table_actions.discord_thread.message.channelId.help'))
->required()
->rules(['required', 'string']),
->required(),
RichEditor::make(DiscordMessage::ATTRIBUTE_CONTENT)
->label(__('filament.table_actions.discord_thread.message.content.name'))
@@ -85,8 +84,7 @@ class DiscordSendMessageTableAction extends BaseTableAction
RichEditor::make(DiscordEmbed::ATTRIBUTE_DESCRIPTION)
->label(__('filament.table_actions.discord_thread.message.embeds.body.description.name'))
->helperText(__('filament.table_actions.discord_thread.message.embeds.body.description.help'))
->required()
->rules(['required', 'string']),
->required(),
ColorPicker::make(DiscordEmbed::ATTRIBUTE_COLOR)
->label(__('filament.table_actions.discord_thread.message.embeds.body.color.name'))
@@ -109,14 +107,12 @@ class DiscordSendMessageTableAction extends BaseTableAction
TextInput::make(DiscordEmbed::ATTRIBUTE_FIELDS_NAME)
->label(__('filament.table_actions.discord_thread.message.embeds.body.fields.name.name'))
->helperText(__('filament.table_actions.discord_thread.message.embeds.body.fields.name.help'))
->required()
->rules(['required', 'string']),
->required(),
TextInput::make(DiscordEmbed::ATTRIBUTE_FIELDS_VALUE)
->label(__('filament.table_actions.discord_thread.message.embeds.body.fields.value.name'))
->helperText(__('filament.table_actions.discord_thread.message.embeds.body.fields.value.help'))
->required()
->rules(['required', 'string']),
->required(),
Checkbox::make(DiscordEmbed::ATTRIBUTE_FIELDS_INLINE)
->label(__('filament.table_actions.discord_thread.message.embeds.body.fields.inline.name'))
@@ -134,8 +130,7 @@ class DiscordSendMessageTableAction extends BaseTableAction
TextInput::make(DiscordMessage::ATTRIBUTE_URL)
->label(__('filament.table_actions.discord_thread.message.images.body.url.name'))
->helperText(__('filament.table_actions.discord_thread.message.images.body.url.help'))
->required()
->rules(['required', 'string']),
->required(),
]),
]);
}
@@ -13,7 +13,6 @@ use App\Models\Wiki\Image;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Form;
use Illuminate\Support\Arr;
use Illuminate\Validation\Rules\Enum;
/**
* Class UploadImageTableAction.
@@ -79,7 +78,7 @@ class UploadImageTableAction extends BaseTableAction
->helperText(__('filament.fields.image.facet.help'))
->options($options)
->required()
->rules(['required', new Enum(ImageFacet::class)]),
->enum(ImageFacet::class),
FileUpload::make(Image::ATTRIBUTE_PATH)
->label(__('filament.fields.image.image.name'))
@@ -69,7 +69,8 @@ abstract class ReconcileStorageTableAction extends ReconcileTableAction implemen
TextInput::make('path')
->label(__('filament.actions.repositories.storage.fields.path.name'))
->required()
->rules(['required', 'string', 'doesnt_start_with:/', new StorageDirectoryExistsRule($fs)])
->doesntStartWith('/')
->rule(new StorageDirectoryExistsRule($fs))
->helperText(__('filament.actions.repositories.storage.fields.path.help')),
]);
}
@@ -127,8 +127,7 @@ abstract class DumpTableAction extends BaseTableAction
TextInput::make('default-character-set')
->label(__('filament.actions.dump.dump.fields.mysql.default_character_set.name'))
->helperText(__('filament.actions.dump.dump.fields.mysql.default_character_set.help'))
->nullable()
->rules(['nullable', 'string', 'max:192']),
->maxLength(192),
Select::make('set-gtid-purged')
->label(__('filament.actions.dump.dump.fields.mysql.set_gtid_purged.name'))
@@ -138,8 +137,7 @@ abstract class DumpTableAction extends BaseTableAction
'ON' => __('filament.actions.dump.dump.fields.mysql.set_gtid_purged.options.on'),
'AUTO' => __('filament.actions.dump.dump.fields.mysql.set_gtid_purged.options.auto'),
])
->nullable()
->rules(['nullable', Rule::in(['OFF', 'ON', 'AUTO'])->__toString()]),
->rule(Rule::in(['OFF', 'ON', 'AUTO'])->__toString()),
Checkbox::make('no-create-info')
->label(__('filament.actions.dump.dump.fields.mysql.no_create_info.name'))
@@ -57,7 +57,9 @@ abstract class UploadTableAction extends StorageTableAction implements Interacts
TextInput::make('path')
->label(__('filament.actions.storage.upload.fields.path.name'))
->helperText(__('filament.actions.storage.upload.fields.path.help'))
->rules(['doesnt_start_with:/', 'doesnt_end_with:/', 'string', new StorageDirectoryExistsRule($fs)])
->doesntStartWith('/')
->doesntEndWith('/')
->rule(new StorageDirectoryExistsRule($fs))
->hidden(fn ($livewire) => $livewire instanceof VideoEntryRelationManager || $livewire instanceof ScriptVideoRelationManager),
]);
}
@@ -57,7 +57,6 @@ use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Enum;
use Illuminate\Validation\Rules\File as FileRule;
/**
@@ -106,39 +105,39 @@ class UploadVideoTableAction extends UploadTableAction
->label(__('filament.fields.video.nc.name'))
->helperText(__('filament.fields.video.nc.help'))
->nullable()
->rules(['nullable', 'boolean']),
->rules(['boolean']),
Checkbox::make(Video::ATTRIBUTE_SUBBED)
->label(__('filament.fields.video.subbed.name'))
->helperText(__('filament.fields.video.subbed.help'))
->nullable()
->rules(['nullable', 'boolean']),
->rules(['boolean']),
Checkbox::make(Video::ATTRIBUTE_LYRICS)
->label(__('filament.fields.video.lyrics.name'))
->helperText(__('filament.fields.video.lyrics.help'))
->nullable()
->rules(['nullable', 'boolean']),
->rules(['boolean']),
Checkbox::make(Video::ATTRIBUTE_UNCEN)
->label(__('filament.fields.video.uncen.name'))
->helperText(__('filament.fields.video.uncen.help'))
->nullable()
->rules(['nullable', 'boolean']),
->rules(['boolean']),
Select::make(Video::ATTRIBUTE_OVERLAP)
->label(__('filament.fields.video.overlap.name'))
->helperText(__('filament.fields.video.overlap.help'))
->options(VideoOverlap::asSelectArray())
->required()
->rules(['required', new Enum(VideoOverlap::class)]),
->enum(VideoOverlap::class),
Select::make(Video::ATTRIBUTE_SOURCE)
->label(__('filament.fields.video.source.name'))
->helperText(__('filament.fields.video.source.help'))
->options(VideoSource::asSelectArray())
->required()
->rules(['required', new Enum(VideoSource::class)]),
->enum(VideoSource::class),
]),
Section::make(__('filament.resources.singularLabel.video_script'))
@@ -146,8 +145,7 @@ class UploadVideoTableAction extends UploadTableAction
FileUpload::make('script')
->label(__('filament.resources.singularLabel.video_script'))
->helperText(__('filament.actions.storage.upload.fields.file.help'))
->nullable()
->rules(['nullable', FileRule::types('txt')->max(2 * 1024)])
->rule(FileRule::types('txt')->max(2 * 1024))
->storeFiles(false),
BelongsTo::make('encoder')
@@ -167,7 +165,7 @@ class UploadVideoTableAction extends UploadTableAction
->helperText(__('filament.actions.video.backfill.fields.should.help'))
->options(ShouldBackfillAudio::asSelectArray())
->required()
->rules(['required', new Enum(ShouldBackfillAudio::class)])
->enum(ShouldBackfillAudio::class)
->default(ShouldBackfillAudio::YES->value),
...BackfillAudioAction::make()->getForm($form)->getComponents(),
@@ -181,7 +179,7 @@ class UploadVideoTableAction extends UploadTableAction
->helperText(__('filament.bulk_actions.discord.notification.should_send.help'))
->options(ShouldSendNotification::asSelectArray())
->required()
->rules(['required', new Enum(ShouldSendNotification::class)])
->enum(ShouldSendNotification::class)
->default(ShouldSendNotification::YES->value),
...VideoDiscordNotificationBulkAction::make()->getForm($form)->getComponents(),
@@ -14,6 +14,14 @@ use Symfony\Component\HttpFoundation\StreamedResponse;
*/
class DumpController extends Controller
{
/**
* Create a new controller instance.
*/
public function __construct()
{
$this->authorizeResource(Dump::class, 'dump');
}
/**
* Download dump.
*
@@ -11,6 +11,7 @@ use App\Actions\Http\Api\RestoreAction;
use App\Actions\Http\Api\ShowAction;
use App\Actions\Http\Api\StoreAction;
use App\Actions\Http\Api\UpdateAction;
use App\Enums\Http\Api\Filter\ComparisonOperator;
use App\Http\Api\Query\Query;
use App\Http\Controllers\Api\BaseController;
use App\Http\Requests\Api\IndexRequest;
@@ -46,7 +47,13 @@ class DumpController extends BaseController
{
$query = new Query($request->validated());
$dumps = $action->index(Dump::query(), $query, $request->schema());
$builder = Dump::query();
foreach (Dump::safeDumps() as $path) {
$builder->orWhere(Dump::ATTRIBUTE_PATH, ComparisonOperator::LIKE->value, $path.'%');
}
$dumps = $action->index($builder, $query, $request->schema());
return new DumpCollection($dumps, $query);
}
+20 -25
View File
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\Models\Admin;
use App\Contracts\Models\HasSubtitle;
use App\Contracts\Models\Nameable;
use App\Discord\DiscordEmbedField;
use App\Enums\Models\Admin\ActionLogStatus;
@@ -40,7 +39,7 @@ use Throwable;
* @property int $user_id
* @property User $user
*/
class ActionLog extends Model implements Nameable, HasSubtitle
class ActionLog extends Model implements Nameable
{
final public const TABLE = 'action_logs';
@@ -173,16 +172,6 @@ class ActionLog extends Model implements Nameable, HasSubtitle
->__toString();
}
/**
* Get subtitle.
*
* @return string
*/
public function getSubtitle(): string
{
return $this->name;
}
/**
* Get the actionable.
*
@@ -243,7 +232,7 @@ class ActionLog extends Model implements Nameable, HasSubtitle
ActionLog::ATTRIBUTE_TARGET_ID => $model->getKey(),
ActionLog::ATTRIBUTE_MODEL_TYPE => $model->getMorphClass(),
ActionLog::ATTRIBUTE_MODEL_ID => $model->getKey(),
ActionLog::ATTRIBUTE_FIELDS => static::getFields($model, $model->getAttributes()),
ActionLog::ATTRIBUTE_FIELDS => static::getFields($model->getAttributes(), $model),
ActionLog::ATTRIBUTE_STATUS => ActionLogStatus::FINISHED->value,
ActionLog::ATTRIBUTE_FINISHED_AT => Date::now(),
]);
@@ -267,7 +256,7 @@ class ActionLog extends Model implements Nameable, HasSubtitle
ActionLog::ATTRIBUTE_TARGET_ID => $model->getKey(),
ActionLog::ATTRIBUTE_MODEL_TYPE => $model->getMorphClass(),
ActionLog::ATTRIBUTE_MODEL_ID => $model->getKey(),
ActionLog::ATTRIBUTE_FIELDS => static::getFields($model, $model->getChanges()),
ActionLog::ATTRIBUTE_FIELDS => static::getFields($model->getChanges(), $model),
ActionLog::ATTRIBUTE_STATUS => ActionLogStatus::FINISHED->value,
ActionLog::ATTRIBUTE_FINISHED_AT => Date::now(),
]);
@@ -314,7 +303,7 @@ class ActionLog extends Model implements Nameable, HasSubtitle
ActionLog::ATTRIBUTE_TARGET_ID => $model->getKey(),
ActionLog::ATTRIBUTE_MODEL_TYPE => $model->getMorphClass(),
ActionLog::ATTRIBUTE_MODEL_ID => $model->getKey(),
ActionLog::ATTRIBUTE_FIELDS => static::getFields($model, $model->getAttributes()),
ActionLog::ATTRIBUTE_FIELDS => static::getFields($model->getAttributes(), $model),
ActionLog::ATTRIBUTE_STATUS => ActionLogStatus::FINISHED->value,
ActionLog::ATTRIBUTE_FINISHED_AT => Date::now(),
]);
@@ -332,6 +321,8 @@ class ActionLog extends Model implements Nameable, HasSubtitle
*/
public static function modelPivot(string $actionName, Model $related, Model $parent, Model $pivot, mixed $action = null): ActionLog
{
$data = $action?->getFormData();
return ActionLog::query()->create([
ActionLog::ATTRIBUTE_BATCH_ID => Str::orderedUuid()->__toString(),
ActionLog::ATTRIBUTE_USER => ActionLog::getUserId(),
@@ -342,7 +333,7 @@ class ActionLog extends Model implements Nameable, HasSubtitle
ActionLog::ATTRIBUTE_TARGET_ID => $parent->getKey(),
ActionLog::ATTRIBUTE_MODEL_TYPE => $pivot->getMorphClass(),
ActionLog::ATTRIBUTE_MODEL_ID => $pivot->getKey(),
ActionLog::ATTRIBUTE_FIELDS => $action?->getFormData() ?? static::getFields($pivot, $pivot->getAttributes()),
ActionLog::ATTRIBUTE_FIELDS => $data ? static::getFields($data) : static::getFields($pivot->getAttributes(), $pivot),
ActionLog::ATTRIBUTE_STATUS => ActionLogStatus::FINISHED->value,
ActionLog::ATTRIBUTE_FINISHED_AT => Date::now(),
]);
@@ -368,7 +359,7 @@ class ActionLog extends Model implements Nameable, HasSubtitle
ActionLog::ATTRIBUTE_TARGET_ID => $parent->getKey(),
ActionLog::ATTRIBUTE_MODEL_TYPE => $related->getMorphClass(),
ActionLog::ATTRIBUTE_MODEL_ID => $related->getKey(),
ActionLog::ATTRIBUTE_FIELDS => static::getFields($related, $related->getAttributes()),
ActionLog::ATTRIBUTE_FIELDS => static::getFields($related->getAttributes(), $related),
ActionLog::ATTRIBUTE_STATUS => ActionLogStatus::FINISHED->value,
ActionLog::ATTRIBUTE_FINISHED_AT => Date::now(),
]);
@@ -394,7 +385,7 @@ class ActionLog extends Model implements Nameable, HasSubtitle
ActionLog::ATTRIBUTE_TARGET_ID => $model->getKey(),
ActionLog::ATTRIBUTE_MODEL_TYPE => $model->getMorphClass(),
ActionLog::ATTRIBUTE_MODEL_ID => $model->getKey(),
ActionLog::ATTRIBUTE_FIELDS => $action->getFormData(),
ActionLog::ATTRIBUTE_FIELDS => static::getFields($action->getFormData()),
ActionLog::ATTRIBUTE_STATUS => ActionLogStatus::RUNNING->value,
]);
}
@@ -495,22 +486,26 @@ class ActionLog extends Model implements Nameable, HasSubtitle
/**
* Format the fields to store.
*
* @param Model $model
* @param array $fields
* @param Model|null $model
* @return array
*/
protected static function getFields(Model $model, array $fields): array
protected static function getFields(array $fields, ?Model $model = null): array
{
return collect($fields)
->forget($model->getCreatedAtColumn())
->forget($model->getUpdatedAtColumn())
->forget(Model::CREATED_AT)
->forget(Model::UPDATED_AT)
->forget(BaseModel::ATTRIBUTE_DELETED_AT)
->map(function ($value, $key) use ($model) {
if (in_array($key, $model->getHidden())) {
return DiscordEmbedField::DEFAULT_FIELD_VALUE;
if ($model instanceof Model) {
if (in_array($key, $model->getHidden())) {
return DiscordEmbedField::DEFAULT_FIELD_VALUE;
}
return DiscordEmbedField::formatEmbedFieldValue($model->getAttribute($key));
}
return DiscordEmbedField::formatEmbedFieldValue($model->getAttribute($key));
return DiscordEmbedField::formatEmbedFieldValue($value);
})
->toArray();
}
+15
View File
@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace App\Models\Admin;
use App\Actions\Storage\Admin\Dump\DumpDocumentAction;
use App\Actions\Storage\Admin\Dump\DumpWikiAction;
use App\Events\Admin\Dump\DumpCreated;
use App\Events\Admin\Dump\DumpDeleted;
use App\Events\Admin\Dump\DumpRestored;
@@ -82,4 +84,17 @@ class Dump extends BaseModel
{
return $this->getName();
}
/**
* Get the available safe dumps.
*
* @return array
*/
public static function safeDumps(): array
{
return [
DumpDocumentAction::FILENAME_PREFIX,
DumpWikiAction::FILENAME_PREFIX,
];
}
}
+12
View File
@@ -131,6 +131,18 @@ class FeaturedTheme extends BaseModel
return $this->animethemeentry === null ? $this->getName() : $this->animethemeentry->getName();
}
/**
* Get the eager loads needed to the subtitle.
*
* @return array
*/
public static function getEagerLoadsForSubtitle(): array
{
return [
FeaturedTheme::RELATION_ENTRY,
];
}
/**
* Get the user that recommended the featured theme.
*
+12 -1
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Models\Auth;
use App\Contracts\Models\Nameable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
use Spatie\Permission\Models\Role as BaseRole;
@@ -20,7 +21,7 @@ use Spatie\Permission\Models\Role as BaseRole;
* @property int $priority
* @property Carbon $updated_at
*/
class Role extends BaseRole
class Role extends BaseRole implements Nameable
{
final public const TABLE = 'roles';
@@ -48,4 +49,14 @@ class Role extends BaseRole
Role::ATTRIBUTE_PRIORITY => 'int',
];
}
/**
* Get name.
*
* @return string
*/
public function getName(): string
{
return $this->name;
}
}
+12
View File
@@ -109,6 +109,18 @@ class DiscordThread extends Model implements Nameable, HasSubtitle
return $this->anime->getName();
}
/**
* Get the eager loads needed to the subtitle.
*
* @return array
*/
public static function getEagerLoadsForSubtitle(): array
{
return [
DiscordThread::RELATION_ANIME,
];
}
/**
* Gets the anime that the thread uses.
*
+12
View File
@@ -118,6 +118,18 @@ class ExternalEntry extends BaseModel
return $this->anime->getName();
}
/**
* Get the eager loads needed to the subtitle.
*
* @return array
*/
public static function getEagerLoadsForSubtitle(): array
{
return [
ExternalEntry::RELATION_ANIME,
];
}
/**
* Get the route key for the model.
*
+12
View File
@@ -103,6 +103,18 @@ class ExternalToken extends BaseModel
return $this->externalprofile->getName();
}
/**
* Get the eager loads needed to the subtitle.
*
* @return array
*/
public static function getEagerLoadsForSubtitle(): array
{
return [
ExternalToken::RELATION_PROFILE,
];
}
/**
* Get the route key for the model.
*
+12
View File
@@ -138,6 +138,18 @@ class ExternalProfile extends BaseModel
return $this->user === null ? $this->getName() : $this->user->getName();
}
/**
* Get the eager loads needed to the subtitle.
*
* @return array
*/
public static function getEagerLoadsForSubtitle(): array
{
return [
ExternalProfile::RELATION_USER,
];
}
/**
* Get the route key for the model.
*
+12
View File
@@ -165,6 +165,18 @@ class Playlist extends BaseModel implements HasHashids, Viewable, HasImages
return $this->user === null ? $this->getName() : $this->user->getName();
}
/**
* Get the eager loads needed to the subtitle.
*
* @return array
*/
public static function getEagerLoadsForSubtitle(): array
{
return [
Playlist::RELATION_USER,
];
}
/**
* Determine if the model should be searchable.
*
+13 -2
View File
@@ -151,9 +151,20 @@ class PlaylistTrack extends BaseModel implements HasHashids, InteractsWithSchema
->append(' - ');
}
$subtitle = $subtitle->append($this->playlist->getName());
return $subtitle->append($this->playlist->getName())->__toString();
}
return $subtitle->__toString();
/**
* Get the eager loads needed to the subtitle.
*
* @return array
*/
public static function getEagerLoadsForSubtitle(): array
{
return [
PlaylistTrack::RELATION_PLAYLIST,
'playlist.user',
];
}
/**
+2
View File
@@ -20,6 +20,8 @@ use Illuminate\Support\Facades\Request;
*/
class View extends BaseView
{
final public const TABLE = 'views';
/**
* Bootstrap the model.
*
+2
View File
@@ -17,6 +17,8 @@ class Notification extends DatabaseNotification
{
use HasFactory;
final public const TABLE = 'notifications';
final public const ATTRIBUTE_ID = 'id';
final public const ATTRIBUTE_TYPE = 'type';
final public const ATTRIBUTE_NOTIFIABLE_TYPE = 'notifiable_type';
+12
View File
@@ -116,6 +116,18 @@ class Report extends Model implements Nameable, HasSubtitle
return strval($this->getKey());
}
/**
* Get the eager loads needed to the subtitle.
*
* @return array
*/
public static function getEagerLoadsForSubtitle(): array
{
return [
Report::RELATION_USER,
];
}
/**
* Get the attributes that should be cast.
*
+12
View File
@@ -113,6 +113,18 @@ class AnimeSynonym extends BaseModel
return $this->anime->getName();
}
/**
* Get the eager loads needed to the subtitle.
*
* @return array
*/
public static function getEagerLoadsForSubtitle(): array
{
return [
AnimeSynonym::RELATION_ANIME,
];
}
/**
* Gets the anime that owns the synonym.
*
+25 -2
View File
@@ -67,6 +67,7 @@ class AnimeTheme extends BaseModel implements InteractsWithSchema
final public const RELATION_GROUP = 'group';
final public const RELATION_IMAGES = 'anime.images';
final public const RELATION_PERFORMANCES = 'song.performances';
final public const RELATION_PERFORMANCES_ARTISTS = 'song.performances.artist';
final public const RELATION_SONG = 'song';
final public const RELATION_SYNONYMS = 'anime.animesynonyms';
final public const RELATION_VIDEOS = 'animethemeentries.videos';
@@ -177,11 +178,21 @@ class AnimeTheme extends BaseModel implements InteractsWithSchema
*/
public function getName(): string
{
$this->loadMissing(AnimeTheme::RELATION_GROUP);
$this->loadMissing([
AnimeTheme::RELATION_SONG,
AnimeTheme::RELATION_GROUP,
]);
return Str::of($this->type->localize())
$name = Str::of($this->type->localize());
if ($this->type === ThemeType::IN && $this->song !== null) {
$name = $name->append(" \"{$this->song->getName()}\" ");
}
return $name
->append($this->type === ThemeType::IN ? '' : strval($this->sequence ?? 1))
->append($this->group !== null ? '-'.$this->group->slug : '')
->trim()
->__toString();
}
@@ -195,6 +206,18 @@ class AnimeTheme extends BaseModel implements InteractsWithSchema
return $this->anime->getName();
}
/**
* Get the eager loads needed to the subtitle.
*
* @return array
*/
public static function getEagerLoadsForSubtitle(): array
{
return [
AnimeTheme::RELATION_ANIME,
];
}
/**
* Gets the anime that owns the theme.
*
@@ -202,6 +202,19 @@ class AnimeThemeEntry extends BaseModel implements InteractsWithSchema
return "{$this->anime->getName()} {$this->animetheme->getName()}";
}
/**
* Get the eager loads needed to the subtitle.
*
* @return array
*/
public static function getEagerLoadsForSubtitle(): array
{
return [
AnimeThemeEntry::RELATION_ANIME_SHALLOW,
AnimeThemeEntry::RELATION_THEME,
];
}
/**
* Get the theme that owns the entry.
*
+13
View File
@@ -50,6 +50,7 @@ class Song extends BaseModel implements HasResources
final public const RELATION_ANIMETHEMES = 'animethemes';
final public const RELATION_ARTISTS = 'artists';
final public const RELATION_PERFORMANCES = 'performances';
final public const RELATION_PERFORMANCE_ARTISTS = 'performances.artist';
final public const RELATION_RESOURCES = 'resources';
final public const RELATION_THEME_GROUPS = 'animethemes.group';
final public const RELATION_VIDEOS = 'animethemes.animethemeentries.videos';
@@ -118,6 +119,18 @@ class Song extends BaseModel implements HasResources
: $this->getName();
}
/**
* Get the eager loads needed to the subtitle.
*
* @return array
*/
public static function getEagerLoadsForSubtitle(): array
{
return [
Song::RELATION_ANIME,
];
}
/**
* Get the anime themes that use this song.
*
+13
View File
@@ -107,6 +107,19 @@ class Membership extends BaseModel
return "Member {$this->member->getName()} of Group {$this->artist->getName()}";
}
/**
* Get the eager loads needed to the subtitle.
*
* @return array
*/
public static function getEagerLoadsForSubtitle(): array
{
return [
Membership::RELATION_ARTIST,
Membership::RELATION_MEMBER,
];
}
/**
* Get the artist that owns the membership.
*
+12
View File
@@ -111,6 +111,18 @@ class Performance extends BaseModel
return $this->song->getName();
}
/**
* Get the eager loads needed to the subtitle.
*
* @return array
*/
public static function getEagerLoadsForSubtitle(): array
{
return [
Performance::RELATION_SONG,
];
}
/**
* Determine if the performance is a membership.
*
+12
View File
@@ -96,6 +96,18 @@ class VideoScript extends BaseModel implements InteractsWithSchema
return $this->video->getName();
}
/**
* Get the eager loads needed to the subtitle.
*
* @return array
*/
public static function getEagerLoadsForSubtitle(): array
{
return [
VideoScript::RELATION_VIDEO,
];
}
/**
* Get the video that owns the script.
*
+41
View File
@@ -4,11 +4,52 @@ declare(strict_types=1);
namespace App\Policies\Admin;
use App\Enums\Auth\CrudPermission;
use App\Enums\Auth\Role;
use App\Models\Admin\Dump;
use App\Models\Auth\User;
use App\Models\BaseModel;
use App\Policies\BasePolicy;
use Filament\Facades\Filament;
use Illuminate\Database\Eloquent\Model;
/**
* Class DumpPolicy.
*/
class DumpPolicy extends BasePolicy
{
/**
* Determine whether the user can view any models.
*
* @param User|null $user
* @return bool
*/
public function viewAny(?User $user): bool
{
if (Filament::isServing()) {
return $user !== null && $user->can(CrudPermission::VIEW->format(static::getModel()));
}
return true;
}
/**
* Determine whether the user can view the model.
*
* @param User|null $user
* @param Dump $dump
* @return bool
*/
public function view(?User $user, BaseModel|Model $dump): bool
{
if (Filament::isServing()) {
return $user !== null && $user->can(CrudPermission::VIEW->format(static::getModel()));
}
if ($user?->hasRole(Role::ADMIN->value)) {
return true;
}
return str($dump->path)->contains(Dump::safeDumps());
}
}
-38
View File
@@ -1,38 +0,0 @@
<?php
declare(strict_types=1);
namespace Database\Seeders\User;
use App\Models\Admin\ActionLog;
use App\Models\User\Encode;
use App\Models\Wiki\Video;
use Illuminate\Database\Seeder;
use Illuminate\Support\Collection;
/**
* Class EncodesSeeder.
*/
class EncodesSeeder extends Seeder
{
/**
* Seed the application's database.
*
* @return void
*/
public function run(): void
{
ActionLog::query()
->where(ActionLog::ATTRIBUTE_NAME, 'Upload Video')
->where(ActionLog::ATTRIBUTE_TARGET_TYPE, Video::class)
->latest()
->chunkById(10, fn (Collection $logs) => $logs->each(function (ActionLog $log) {
Encode::query()
->firstOrCreate([
Encode::ATTRIBUTE_VIDEO => $log->target_id,
], [
Encode::ATTRIBUTE_USER => $log->user_id,
]);
}));
}
}
+5 -1
View File
@@ -477,7 +477,11 @@ return [
'target' => 'Target',
'status' => 'Status',
'happened_at' => 'Happened At',
'fields' => 'Fields',
'fields' => [
'name' => 'Fields',
'keys' => 'Fields',
'values' => 'Values',
],
'finished_at' => 'Finished At',
'exception' => 'Exception',
],
@@ -9,9 +9,11 @@
@endif
<div class="flex flex-col justify-center pl-3 py-2">
<p class="text-sm font-bold pb-1">{{ $name }}</p>
@if ($subtitle !== null)
<div class="flex flex-col items-start">
<p class="text-xs leading-5">{{ $subtitle }}</p>
</div>
@endif
</div>
</div>
</div>