From 696a9882a96d1681a6b487e3245bcd0a592ed27e Mon Sep 17 00:00:00 2001 From: Kyrch Date: Tue, 9 Dec 2025 12:04:48 -0300 Subject: [PATCH] feat: WIP submission panel & submission stages (#1018) --- .../User/Submission/ManageStepAction.php | 97 ------ .../User/Submission/SubmissionAction.php | 99 ------ .../Storage/Admin/Dump/DumpUserAction.php | 4 +- app/Concerns/Models/Submitable.php | 6 +- app/Enums/Models/User/ApprovableStatus.php | 32 -- .../Models/User/SubmissionActionType.php | 19 -- app/Enums/Models/User/SubmissionStatus.php | 34 ++ .../Components/Fields/SubmissionBelongsTo.php | 112 +++++++ .../User/SubmissionStageRelationManager.php | 30 ++ .../User/SubmissionStepRelationManager.php | 30 -- app/Filament/Resources/User/Submission.php | 14 +- ...php => StageSubmissionRelationManager.php} | 10 +- .../User/Submission/SubmissionStage.php | 118 +++++++ .../Pages/ListSubmissionStages.php | 13 + .../Pages/ViewSubmissionStage.php | 13 + .../User/Submission/SubmissionStep.php | 196 ----------- .../Pages/ListSubmissionSteps.php | 13 - .../Pages/ViewSubmissionStep.php | 13 - app/Filament/Resources/Wiki/Anime/Synonym.php | 3 +- app/Filament/Resources/Wiki/Anime/Theme.php | 32 +- .../Wiki/Anime/Theme/Schemas/ThemeForm.php | 50 +++ .../Anime/Pages/CreateAnimeSubmission.php | 307 ++++++++++++++++++ .../Anime/Pages/ListAnimeSubmissions.php | 26 ++ .../Resources/AnimeSubmissionResource.php | 140 ++++++++ .../Resources/BaseSubmissionResource.php | 144 ++++++++ .../Admin/UserExceedsSubmissionLimit.php | 4 +- app/Models/Auth/User.php | 5 + app/Models/User/Submission.php | 50 ++- .../User/Submission/SubmissionStage.php | 110 +++++++ app/Models/User/Submission/SubmissionStep.php | 169 ---------- ...epPolicy.php => SubmissionStagePolicy.php} | 24 +- app/Policies/User/SubmissionPolicy.php | 2 +- .../Filament/SubmissionPanelProvider.php | 53 +++ config/app.php | 1 + .../Submission/SubmissionStageFactory.php | 38 +++ .../User/Submission/SubmissionStepFactory.php | 45 --- database/factories/User/SubmissionFactory.php | 4 +- ...12_09_141202_create_submissions_table.php} | 32 +- lang/en/enums.php | 21 +- lang/en/filament.php | 27 +- lang/en/submissions.php | 14 + .../User/Submission/SubmissionStageTest.php | 45 +++ .../User/Submission/SubmissionStepTest.php | 72 ---- tests/Unit/Models/User/SubmissionTest.php | 20 +- 44 files changed, 1370 insertions(+), 921 deletions(-) delete mode 100644 app/Actions/Models/User/Submission/ManageStepAction.php delete mode 100644 app/Actions/Models/User/Submission/SubmissionAction.php delete mode 100644 app/Enums/Models/User/ApprovableStatus.php delete mode 100644 app/Enums/Models/User/SubmissionActionType.php create mode 100644 app/Enums/Models/User/SubmissionStatus.php create mode 100644 app/Filament/Components/Fields/SubmissionBelongsTo.php create mode 100644 app/Filament/RelationManagers/User/SubmissionStageRelationManager.php delete mode 100644 app/Filament/RelationManagers/User/SubmissionStepRelationManager.php rename app/Filament/Resources/User/Submission/RelationManagers/{StepSubmissionRelationManager.php => StageSubmissionRelationManager.php} (60%) create mode 100644 app/Filament/Resources/User/Submission/SubmissionStage.php create mode 100644 app/Filament/Resources/User/Submission/SubmissionStage/Pages/ListSubmissionStages.php create mode 100644 app/Filament/Resources/User/Submission/SubmissionStage/Pages/ViewSubmissionStage.php delete mode 100644 app/Filament/Resources/User/Submission/SubmissionStep.php delete mode 100644 app/Filament/Resources/User/Submission/SubmissionStep/Pages/ListSubmissionSteps.php delete mode 100644 app/Filament/Resources/User/Submission/SubmissionStep/Pages/ViewSubmissionStep.php create mode 100644 app/Filament/Resources/Wiki/Anime/Theme/Schemas/ThemeForm.php create mode 100644 app/Filament/Submission/Resources/Anime/Pages/CreateAnimeSubmission.php create mode 100644 app/Filament/Submission/Resources/Anime/Pages/ListAnimeSubmissions.php create mode 100644 app/Filament/Submission/Resources/AnimeSubmissionResource.php create mode 100644 app/Filament/Submission/Resources/BaseSubmissionResource.php create mode 100644 app/Models/User/Submission/SubmissionStage.php delete mode 100644 app/Models/User/Submission/SubmissionStep.php rename app/Policies/User/Submission/{SubmissionStepPolicy.php => SubmissionStagePolicy.php} (61%) create mode 100644 app/Providers/Filament/SubmissionPanelProvider.php create mode 100644 database/factories/User/Submission/SubmissionStageFactory.php delete mode 100644 database/factories/User/Submission/SubmissionStepFactory.php rename database/migrations/{2025_11_28_195436_create_submissions_table.php => 2025_12_09_141202_create_submissions_table.php} (55%) create mode 100644 lang/en/submissions.php create mode 100644 tests/Unit/Models/User/Submission/SubmissionStageTest.php delete mode 100644 tests/Unit/Models/User/Submission/SubmissionStepTest.php diff --git a/app/Actions/Models/User/Submission/ManageStepAction.php b/app/Actions/Models/User/Submission/ManageStepAction.php deleted file mode 100644 index 9b93e7f4b..000000000 --- a/app/Actions/Models/User/Submission/ManageStepAction.php +++ /dev/null @@ -1,97 +0,0 @@ -action) { - SubmissionActionType::CREATE => static::approveCreate($step), - SubmissionActionType::DELETE => static::approveDelete($step), - SubmissionActionType::UPDATE => static::approveUpdate($step), - SubmissionActionType::ATTACH => static::approveAttach($step), - SubmissionActionType::DETACH => static::approveDetach($step), - default => null, - }; - } - - protected static function approveCreate(SubmissionStep $step): void - { - /** @var Model $model */ - $model = new $step->actionable_type; - - $model::query()->create($step->fields); - - static::markAsApproved($step); - } - - protected static function approveDelete(SubmissionStep $step): void - { - $step->actionable->delete(); - - static::markAsApproved($step); - } - - protected static function approveUpdate(SubmissionStep $step): void - { - $step->actionable->update($step->fields); - - static::markAsApproved($step); - } - - protected static function approveAttach(SubmissionStep $step): void - { - /** @var Pivot $pivot */ - $pivot = new $step->pivot; - - $pivot::query()->create($step->fields); - - static::markAsApproved($step); - } - - protected static function approveDetach(SubmissionStep $step): void - { - /** @var Pivot $pivot */ - $pivot = new $step->pivot; - - $pivot::query()->where($step->fields)->delete(); - - static::markAsApproved($step); - } - - protected static function markAsApproved(SubmissionStep $step): void - { - static::updateStatus($step, ApprovableStatus::APPROVED); - } - - protected static function markAsRejected(SubmissionStep $step): void - { - static::updateStatus($step, ApprovableStatus::REJECTED); - } - - protected static function markAsPartiallyApproved(SubmissionStep $step): void - { - static::updateStatus($step, ApprovableStatus::PARTIALLY_APPROVED); - } - - protected static function updateStatus(SubmissionStep $step, ApprovableStatus $status): void - { - $step->update([ - SubmissionStep::ATTRIBUTE_STATUS => $status->value, - SubmissionStep::ATTRIBUTE_FINISHED_AT => Date::now(), - ]); - } -} diff --git a/app/Actions/Models/User/Submission/SubmissionAction.php b/app/Actions/Models/User/Submission/SubmissionAction.php deleted file mode 100644 index ffd031d11..000000000 --- a/app/Actions/Models/User/Submission/SubmissionAction.php +++ /dev/null @@ -1,99 +0,0 @@ -create([ - Submission::ATTRIBUTE_USER => Auth::id(), - Submission::ATTRIBUTE_STATUS => ApprovableStatus::PENDING->value, - Submission::ATTRIBUTE_NOTES => $notes, - ]); - - $submission->steps()->saveMany(Arr::wrap($steps)); - - return $submission; - } - - /** - * Create a submission step to create a model. - * - * @param class-string $model - */ - public static function makeForCreate(string $model, array $fields): SubmissionStep - { - return static::makeFor(SubmissionActionType::CREATE, $model, $fields); - } - - /** - * Create a submission step to edit a model. - */ - public static function makeForUpdate(Model $model, array $fields): SubmissionStep - { - return static::makeFor(SubmissionActionType::UPDATE, $model, $fields); - } - - /** - * Create a submission step to delete a model. - */ - public static function makeForDelete(Model $model): SubmissionStep - { - return static::makeFor(SubmissionActionType::DELETE, $model); - } - - /** - * Create a submission step to attach a model to another in a many-to-many relationship. - * - * @param class-string $pivot - */ - public static function makeForAttach(Model $foreign, Model $related, string $pivot, array $fields): SubmissionStep - { - return static::makeFor(SubmissionActionType::ATTACH, $foreign, $fields, $related, $pivot); - } - - /** - * Create a submission step to detach a model from another in a many-to-many relationship. - */ - public static function makeForDetach(Model $foreign, Model $related, Pivot $pivot, array $fields): SubmissionStep - { - return static::makeFor(SubmissionActionType::DETACH, $foreign, $fields, $related, $pivot); - } - - /** - * Create a submission step for given action. - * - * @param class-string|Model $model - */ - protected static function makeFor(SubmissionActionType $action, Model|string $model, ?array $fields = null, ?Model $related = null, Pivot|string|null $pivot = null): SubmissionStep - { - return new SubmissionStep([ - SubmissionStep::ATTRIBUTE_ACTION => $action->value, - SubmissionStep::ATTRIBUTE_ACTIONABLE_TYPE => $model instanceof Model ? Relation::getMorphAlias($model->getMorphClass()) : $model, - SubmissionStep::ATTRIBUTE_ACTIONABLE_ID => $model instanceof Model ? $model->getKey() : null, - SubmissionStep::ATTRIBUTE_FIELDS => Arr::where($fields, fn ($value, $key) => $model->isFillable($key)), - SubmissionStep::ATTRIBUTE_STATUS => ApprovableStatus::PENDING->value, - SubmissionStep::ATTRIBUTE_TARGET_TYPE => $related instanceof Model ? Relation::getMorphAlias($related->getMorphClass()) : null, - SubmissionStep::ATTRIBUTE_TARGET_ID => $related instanceof Model ? $related->getKey() : null, - SubmissionStep::ATTRIBUTE_PIVOT => $pivot instanceof Model ? Relation::getMorphAlias($pivot->getMorphClass()) : $pivot, - ]); - } -} diff --git a/app/Actions/Storage/Admin/Dump/DumpUserAction.php b/app/Actions/Storage/Admin/Dump/DumpUserAction.php index afcb965ba..dcf496a00 100644 --- a/app/Actions/Storage/Admin/Dump/DumpUserAction.php +++ b/app/Actions/Storage/Admin/Dump/DumpUserAction.php @@ -9,7 +9,7 @@ use App\Models\Aggregate\LikeAggregate; use App\Models\User\Like; use App\Models\User\Notification; use App\Models\User\Submission; -use App\Models\User\Submission\SubmissionStep; +use App\Models\User\Submission\SubmissionStage; use Illuminate\Support\Facades\Date; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; @@ -30,7 +30,7 @@ class DumpUserAction extends DumpAction LikeAggregate::TABLE, Notification::TABLE, Submission::TABLE, - SubmissionStep::TABLE, + SubmissionStage::TABLE, ]; } diff --git a/app/Concerns/Models/Submitable.php b/app/Concerns/Models/Submitable.php index 0cefac534..74906fd75 100644 --- a/app/Concerns/Models/Submitable.php +++ b/app/Concerns/Models/Submitable.php @@ -4,13 +4,13 @@ declare(strict_types=1); namespace App\Concerns\Models; -use App\Models\User\Submission\SubmissionStep; +use App\Models\User\Submission; use Illuminate\Database\Eloquent\Relations\MorphMany; trait Submitable { - public function submissionsteps(): MorphMany + public function submissions(): MorphMany { - return $this->morphMany(SubmissionStep::class, SubmissionStep::RELATION_ACTIONABLE); + return $this->morphMany(Submission::class, Submission::RELATION_ACTIONABLE); } } diff --git a/app/Enums/Models/User/ApprovableStatus.php b/app/Enums/Models/User/ApprovableStatus.php deleted file mode 100644 index 036af4ef8..000000000 --- a/app/Enums/Models/User/ApprovableStatus.php +++ /dev/null @@ -1,32 +0,0 @@ - 'warning', - ApprovableStatus::REJECTED => 'danger', - ApprovableStatus::PARTIALLY_APPROVED => 'info', - ApprovableStatus::APPROVED => 'success', - }; - } -} diff --git a/app/Enums/Models/User/SubmissionActionType.php b/app/Enums/Models/User/SubmissionActionType.php deleted file mode 100644 index 423d755c9..000000000 --- a/app/Enums/Models/User/SubmissionActionType.php +++ /dev/null @@ -1,19 +0,0 @@ - 'warning', + SubmissionStatus::REJECTED => 'danger', + SubmissionStatus::PARTIALLY_APPROVED => 'info', + SubmissionStatus::APPROVED => 'success', + }; + } +} diff --git a/app/Filament/Components/Fields/SubmissionBelongsTo.php b/app/Filament/Components/Fields/SubmissionBelongsTo.php new file mode 100644 index 000000000..d350b4b24 --- /dev/null +++ b/app/Filament/Components/Fields/SubmissionBelongsTo.php @@ -0,0 +1,112 @@ +hiddenFieldName = $name; + + return $this; + } + + public function showCreateOption(): static + { + $this->createOptionForm(fn (Schema $schema): array => $this->resource::form($schema)->getComponents()) + ->editOptionForm(fn (Schema $schema): array => $this->resource::form($schema)->getComponents()) + ->createOptionUsing(function (Set $set, array $data, SubmissionBelongsTo $component): int { + $component->state(-1); + $component->refreshSelectedOptionLabel(); + $set($this->hiddenFieldName, $data); + + return -1; + }) + ->updateOptionUsing(function (Set $set, array $data, SubmissionBelongsTo $component): int { + $component->state(-1); + $component->refreshSelectedOptionLabel(); + $set($this->hiddenFieldName, $data); + + return -1; + }) + ->fillEditOptionActionFormUsing(fn (Get $get): mixed => $get($this->hiddenFieldName)); + + $model = $this->modelResource; + $this->allowHtml(); + $this->getOptionLabelUsing(function (Get $get, $state) use ($model): string { + if ($state === '-1') { + return BelongsTo::getSearchLabelWithBlade(new $model($get($this->hiddenFieldName))); + } + + return BelongsTo::getSearchLabelWithBlade($model::find($state)); + }); + + return $this; + } + + public function withSubtitle(bool $condition = true): static + { + $this->withSubtitle = $condition; + + return $this; + } + + /** + * @param class-string $modelClass + */ + protected function tryScout(string $modelClass): static + { + $this->searchable(); + + if (in_array(Searchable::class, class_uses_recursive($modelClass))) { + return $this + ->getSearchResultsUsing( + fn (string $search) => collect( + Search::search($modelClass, new Criteria($this->escapeReservedChars($search))) + ->execute() + ->items() + ) + ->mapWithKeys(fn (Model $model): array => [$model->getKey() => static::getSearchLabelWithBlade($model, $this->withSubtitle)]) + ->toArray() + ); + } + + return $this + ->getSearchResultsUsing( + fn (string $search) => $modelClass::query() + ->where($this->resource->getRecordTitleAttribute(), ComparisonOperator::LIKE->value, "%$search%") + ->take(25) + ->get() + ->mapWithKeys(fn ($model): array => [$model->getKey() => static::getSearchLabelWithBlade($model, $this->withSubtitle)]) + ->toArray() + ); + } + + /** + * @param Model|User|Nameable|HasSubtitle $model + */ + public static function getSearchLabelWithBlade($model, bool $withSubtitle = true): string + { + return view('filament.components.select') + ->with('name', $model->getName()) + ->with('subtitle', $withSubtitle ? $model->getSubtitle() : null) + ->with('image', $model instanceof User ? $model->getFilamentAvatarUrl() : null) + ->render(); + } +} diff --git a/app/Filament/RelationManagers/User/SubmissionStageRelationManager.php b/app/Filament/RelationManagers/User/SubmissionStageRelationManager.php new file mode 100644 index 000000000..02d194a46 --- /dev/null +++ b/app/Filament/RelationManagers/User/SubmissionStageRelationManager.php @@ -0,0 +1,30 @@ +|null + */ + protected static ?string $relatedResource = SubmissionStageResource::class; + + public function table(Table $table): Table + { + return parent::table( + $table + ->recordTitleAttribute(SubmissionStageResource::getRecordTitleAttribute()) + ->defaultSort(SubmissionStage::TABLE.'.'.SubmissionStage::ATTRIBUTE_ID, 'desc') + ); + } +} diff --git a/app/Filament/RelationManagers/User/SubmissionStepRelationManager.php b/app/Filament/RelationManagers/User/SubmissionStepRelationManager.php deleted file mode 100644 index ef9ca7449..000000000 --- a/app/Filament/RelationManagers/User/SubmissionStepRelationManager.php +++ /dev/null @@ -1,30 +0,0 @@ -|null - */ - protected static ?string $relatedResource = SubmissionStepResource::class; - - public function table(Table $table): Table - { - return parent::table( - $table - ->recordTitleAttribute(SubmissionStepResource::getRecordTitleAttribute()) - ->defaultSort(SubmissionStep::TABLE.'.'.SubmissionStep::ATTRIBUTE_ID, 'desc') - ); - } -} diff --git a/app/Filament/Resources/User/Submission.php b/app/Filament/Resources/User/Submission.php index 6b4d0800e..d465d4055 100644 --- a/app/Filament/Resources/User/Submission.php +++ b/app/Filament/Resources/User/Submission.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace App\Filament\Resources\User; use App\Enums\Filament\NavigationGroup; -use App\Enums\Models\User\ApprovableStatus; +use App\Enums\Models\User\SubmissionStatus; use App\Filament\Components\Columns\BelongsToColumn; use App\Filament\Components\Columns\TextColumn; use App\Filament\Components\Infolist\BelongsToEntry; @@ -15,7 +15,7 @@ use App\Filament\Resources\Auth\User as UserResource; use App\Filament\Resources\BaseResource; use App\Filament\Resources\User\Submission\Pages\ListSubmissions; use App\Filament\Resources\User\Submission\Pages\ViewSubmission; -use App\Filament\Resources\User\Submission\RelationManagers\StepSubmissionRelationManager; +use App\Filament\Resources\User\Submission\RelationManagers\StageSubmissionRelationManager; use App\Models\User\Submission as SubmissionModel; use Filament\Resources\RelationManagers\RelationGroup; use Filament\Schemas\Components\Section; @@ -89,7 +89,7 @@ class Submission extends BaseResource TextColumn::make(SubmissionModel::ATTRIBUTE_STATUS) ->label(__('filament.fields.submission.status')) - ->formatStateUsing(fn (ApprovableStatus $state): ?string => $state->localize()) + ->formatStateUsing(fn (SubmissionStatus $state): ?string => $state->localize()) ->badge(), BelongsToColumn::make(SubmissionModel::RELATION_USER, UserResource::class), @@ -114,7 +114,7 @@ class Submission extends BaseResource TextEntry::make(SubmissionModel::ATTRIBUTE_STATUS) ->label(__('filament.fields.submission.status')) - ->formatStateUsing(fn (ApprovableStatus $state): ?string => $state->localize()) + ->formatStateUsing(fn (SubmissionStatus $state): ?string => $state->localize()) ->badge(), BelongsToEntry::make(SubmissionModel::RELATION_USER, UserResource::class), @@ -125,10 +125,6 @@ class Submission extends BaseResource TextEntry::make(SubmissionModel::ATTRIBUTE_FINISHED_AT) ->label(__('filament.fields.submission.finished_at')) ->dateTime(), - - TextEntry::make(SubmissionModel::ATTRIBUTE_NOTES) - ->label(__('filament.fields.submission.notes')) - ->columnSpanFull(), ]) ->columns(3), @@ -143,7 +139,7 @@ class Submission extends BaseResource { return [ RelationGroup::make(static::getModelLabel(), [ - StepSubmissionRelationManager::class, + StageSubmissionRelationManager::class, ]), ]; } diff --git a/app/Filament/Resources/User/Submission/RelationManagers/StepSubmissionRelationManager.php b/app/Filament/Resources/User/Submission/RelationManagers/StageSubmissionRelationManager.php similarity index 60% rename from app/Filament/Resources/User/Submission/RelationManagers/StepSubmissionRelationManager.php rename to app/Filament/Resources/User/Submission/RelationManagers/StageSubmissionRelationManager.php index 828453145..76eadf47f 100644 --- a/app/Filament/Resources/User/Submission/RelationManagers/StepSubmissionRelationManager.php +++ b/app/Filament/Resources/User/Submission/RelationManagers/StageSubmissionRelationManager.php @@ -4,23 +4,23 @@ declare(strict_types=1); namespace App\Filament\Resources\User\Submission\RelationManagers; -use App\Filament\RelationManagers\User\SubmissionStepRelationManager; +use App\Filament\RelationManagers\User\SubmissionStageRelationManager; use App\Models\User\Submission; -use App\Models\User\Submission\SubmissionStep; +use App\Models\User\Submission\SubmissionStage; use Filament\Tables\Table; -class StepSubmissionRelationManager extends SubmissionStepRelationManager +class StageSubmissionRelationManager extends SubmissionStageRelationManager { /** * The relationship the relation manager corresponds to. */ - protected static string $relationship = Submission::RELATION_STEPS; + protected static string $relationship = Submission::RELATION_STAGES; public function table(Table $table): Table { return parent::table( $table - ->inverseRelationship(SubmissionStep::RELATION_SUBMISSION) + ->inverseRelationship(SubmissionStage::RELATION_SUBMISSION) ); } } diff --git a/app/Filament/Resources/User/Submission/SubmissionStage.php b/app/Filament/Resources/User/Submission/SubmissionStage.php new file mode 100644 index 000000000..68d210978 --- /dev/null +++ b/app/Filament/Resources/User/Submission/SubmissionStage.php @@ -0,0 +1,118 @@ +|null + */ + protected static ?string $model = SubmissionStageModel::class; + + public static function getModelLabel(): string + { + return __('filament.resources.singularLabel.submission_stage'); + } + + public static function getPluralModelLabel(): string + { + return __('filament.resources.label.submission_stages'); + } + + public static function getNavigationGroup(): NavigationGroup + { + return NavigationGroup::USER; + } + + public static function getNavigationIcon(): Heroicon + { + return Heroicon::OutlinedLightBulb; + } + + public static function getRecordSlug(): string + { + return 'submission-stages'; + } + + public static function getRecordTitleAttribute(): string + { + return SubmissionStageModel::ATTRIBUTE_ID; + } + + public static function getEloquentQuery(): Builder + { + $query = parent::getEloquentQuery(); + + // Necessary to prevent lazy loading when loading related resources + return $query->with([ + SubmissionStageModel::RELATION_SUBMISSION, + ]); + } + + public static function form(Schema $schema): Schema + { + return $schema; + } + + public static function table(Table $table): Table + { + return parent::table($table) + ->columns([ + TextColumn::make(SubmissionStageModel::ATTRIBUTE_ID) + ->label(__('filament.fields.base.id')), + + BelongsToColumn::make(SubmissionStageModel::RELATION_SUBMISSION, SubmissionResource::class), + ]); + } + + public static function infolist(Schema $schema): Schema + { + return $schema + ->components([ + Section::make(static::getRecordTitle($schema->getRecord())) + ->schema([ + TextEntry::make(SubmissionStageModel::ATTRIBUTE_ID) + ->label(__('filament.fields.base.id')), + + BelongsToEntry::make(SubmissionStageModel::RELATION_SUBMISSION, SubmissionResource::class) + ->label(__('filament.resources.singularLabel.submission')), + ]) + ->columns(3), + + TimestampSection::make(), + ]); + } + + /** + * @return array + */ + public static function getPages(): array + { + return [ + 'index' => ListSubmissionStages::route('/'), + 'view' => ViewSubmissionStage::route('/{record:stage_id}'), + ]; + } +} diff --git a/app/Filament/Resources/User/Submission/SubmissionStage/Pages/ListSubmissionStages.php b/app/Filament/Resources/User/Submission/SubmissionStage/Pages/ListSubmissionStages.php new file mode 100644 index 000000000..83b1ed7af --- /dev/null +++ b/app/Filament/Resources/User/Submission/SubmissionStage/Pages/ListSubmissionStages.php @@ -0,0 +1,13 @@ +|null - */ - protected static ?string $model = SubmissionStepModel::class; - - public static function getModelLabel(): string - { - return __('filament.resources.singularLabel.submission_step'); - } - - public static function getPluralModelLabel(): string - { - return __('filament.resources.label.submission_steps'); - } - - public static function getNavigationGroup(): NavigationGroup - { - return NavigationGroup::USER; - } - - public static function getNavigationIcon(): Heroicon - { - return Heroicon::OutlinedLightBulb; - } - - public static function getRecordSlug(): string - { - return 'submission-steps'; - } - - public static function getRecordTitleAttribute(): string - { - return SubmissionStepModel::ATTRIBUTE_ID; - } - - public static function getEloquentQuery(): Builder - { - $query = parent::getEloquentQuery(); - - // Necessary to prevent lazy loading when loading related resources - return $query->with([ - SubmissionStepModel::RELATION_SUBMISSION, - ]); - } - - public static function form(Schema $schema): Schema - { - return $schema; - } - - public static function table(Table $table): Table - { - return parent::table($table) - ->columns([ - TextColumn::make(SubmissionStepModel::ATTRIBUTE_ID) - ->label(__('filament.fields.base.id')), - - TextColumn::make(SubmissionStepModel::ATTRIBUTE_ACTION) - ->label(__('filament.fields.submission_step.actionable')) - ->html() - ->formatStateUsing(fn (SubmissionStepModel $record): string => class_basename($record->actionable_type)), - - TextColumn::make(SubmissionStepModel::ATTRIBUTE_STATUS) - ->label(__('filament.fields.submission.status')) - ->formatStateUsing(fn (ApprovableStatus $state): ?string => $state->localize()) - ->badge(), - - TextColumn::make(SubmissionStepModel::ATTRIBUTE_FINISHED_AT) - ->label(__('filament.fields.submission.finished_at')) - ->dateTime(), - - BelongsToColumn::make(SubmissionStepModel::RELATION_SUBMISSION, SubmissionResource::class), - ]); - } - - public static function infolist(Schema $schema): Schema - { - return $schema - ->components([ - Section::make(static::getRecordTitle($schema->getRecord())) - ->schema([ - TextEntry::make(SubmissionStepModel::ATTRIBUTE_ID) - ->label(__('filament.fields.base.id')), - - TextEntry::make(SubmissionStepModel::ATTRIBUTE_ACTION) - ->label(__('filament.fields.submission_step.action')) - ->html() - ->formatStateUsing(fn (SubmissionStepModel $record, SubmissionActionType $state): string => static::getActionName($record, $state)), - - TextEntry::make(SubmissionStepModel::ATTRIBUTE_STATUS) - ->label(__('filament.fields.submission.status')) - ->formatStateUsing(fn (ApprovableStatus $state): ?string => $state->localize()) - ->badge(), - - TextEntry::make(SubmissionStepModel::ATTRIBUTE_FINISHED_AT) - ->label(__('filament.fields.submission.finished_at')) - ->dateTime(), - - BelongsToEntry::make(SubmissionStepModel::RELATION_SUBMISSION, SubmissionResource::class) - ->label(__('filament.resources.singularLabel.submission')), - - KeyValueThreeEntry::make(SubmissionStepModel::ATTRIBUTE_FIELDS) - ->label(__('filament.fields.submission_step.fields.name')) - ->leftLabel(__('filament.fields.submission_step.fields.columns')) - ->middleLabel(__('filament.fields.submission_step.fields.old_values')) - ->rightLabel(__('filament.fields.submission_step.fields.values')) - ->middleValueThroughState(fn (SubmissionStepModel $record): array => $record->formatFields($record->actionable->attributesToArray())) - ->visible(fn (SubmissionStepModel $record): bool => $record->action === SubmissionActionType::UPDATE) - ->state(fn (SubmissionStepModel $record): array => $record->formatFields()) - ->columnSpanFull(), - - KeyValueEntry::make(SubmissionStepModel::ATTRIBUTE_FIELDS) - ->label(__('filament.fields.submission_step.fields.name')) - ->keyLabel(__('filament.fields.submission_step.fields.columns')) - ->valueLabel(__('filament.fields.submission_step.fields.values')) - ->hidden(fn (?array $state, SubmissionStepModel $record): bool => is_null($state) || $record->action === SubmissionActionType::UPDATE) - ->columnSpanFull(), - ]) - ->columns(3), - - TimestampSection::make(), - ]); - } - - /** - * The title of the submission step. - */ - protected static function getActionName(SubmissionStepModel $record, SubmissionActionType $state): string - { - $name = $record->actionable instanceof Nameable ? $record->actionable->getName() : $record->actionable_type; - - if ($state === SubmissionActionType::CREATE) { - return $record->action->localize().' '.$name; - } - - $actionableUrl = Filament::getUrl($record->actionable); - $actionableLink = "{$name}"; - - if ($state === SubmissionActionType::ATTACH || $state === SubmissionActionType::DETACH) { - $targetUrl = Filament::getUrl($record->target); - - $targetName = $record->target instanceof Nameable ? $record->target->getName() : $record->target_type; - - $targetLink = "{$targetName}"; - - return $state->localize().' '.$actionableLink.' to '.$targetLink.' via '.class_basename($record->pivot); - } - - return $record->action->localize().' '.$actionableLink; - } - - /** - * @return array - */ - public static function getPages(): array - { - return [ - 'index' => ListSubmissionSteps::route('/'), - 'view' => ViewSubmissionStep::route('/{record:step_id}'), - ]; - } -} diff --git a/app/Filament/Resources/User/Submission/SubmissionStep/Pages/ListSubmissionSteps.php b/app/Filament/Resources/User/Submission/SubmissionStep/Pages/ListSubmissionSteps.php deleted file mode 100644 index 16c15c325..000000000 --- a/app/Filament/Resources/User/Submission/SubmissionStep/Pages/ListSubmissionSteps.php +++ /dev/null @@ -1,13 +0,0 @@ -components([ BelongsTo::make(SynonymModel::ATTRIBUTE_ANIME) ->resource(AnimeResource::class) - ->hiddenOn(SynonymRelationManager::class), + ->hiddenOn([SynonymRelationManager::class, CreateAnimeSubmission::class]), Select::make(SynonymModel::ATTRIBUTE_TYPE) ->label(__('filament.fields.anime_synonym.type.name')) diff --git a/app/Filament/Resources/Wiki/Anime/Theme.php b/app/Filament/Resources/Wiki/Anime/Theme.php index 0e3f2ce7a..0ee9d15bc 100644 --- a/app/Filament/Resources/Wiki/Anime/Theme.php +++ b/app/Filament/Resources/Wiki/Anime/Theme.php @@ -10,8 +10,6 @@ use App\Filament\Actions\Models\Wiki\Song\LoadArtistsAction; use App\Filament\Components\Columns\BelongsToColumn; use App\Filament\Components\Columns\TextColumn; use App\Filament\Components\Fields\BelongsTo; -use App\Filament\Components\Fields\Select; -use App\Filament\Components\Fields\TextInput; use App\Filament\Components\Infolist\BelongsToEntry; use App\Filament\Components\Infolist\TextEntry; use App\Filament\Components\Infolist\TimestampSection; @@ -23,6 +21,7 @@ use App\Filament\Resources\Wiki\Anime\Theme\Entry; use App\Filament\Resources\Wiki\Anime\Theme\Pages\ListThemes; use App\Filament\Resources\Wiki\Anime\Theme\Pages\ViewTheme; use App\Filament\Resources\Wiki\Anime\Theme\RelationManagers\EntryThemeRelationManager; +use App\Filament\Resources\Wiki\Anime\Theme\Schemas\ThemeForm; use App\Filament\Resources\Wiki\Artist as ArtistResource; use App\Filament\Resources\Wiki\Group as GroupResource; use App\Filament\Resources\Wiki\Song as SongResource; @@ -136,30 +135,9 @@ class Theme extends BaseResource ->hiddenOn(ThemeRelationManager::class) ->required(), - Select::make(ThemeModel::ATTRIBUTE_TYPE) - ->label(__('filament.fields.anime_theme.type.name')) - ->helperText(__('filament.fields.anime_theme.type.help')) - ->options(ThemeType::class) - ->required() - ->live() - ->partiallyRenderComponentsAfterStateUpdated([ThemeModel::ATTRIBUTE_SLUG]) - ->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')) - ->integer() - ->live() - ->partiallyRenderComponentsAfterStateUpdated([ThemeModel::ATTRIBUTE_SLUG]) - ->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() - ->maxLength(192) - ->alphaDash() - ->readOnly(), + ThemeForm::typeField(), + ThemeForm::sequenceField(), + ThemeForm::slugField(), BelongsTo::make(ThemeModel::ATTRIBUTE_GROUP) ->resource(GroupResource::class) @@ -327,7 +305,7 @@ class Theme extends BaseResource /** * Set the theme slug. */ - protected static function setThemeSlug(Set $set, Get $get): void + public static function setThemeSlug(Set $set, Get $get): void { $slug = Str::of(''); $type = $get(ThemeModel::ATTRIBUTE_TYPE); diff --git a/app/Filament/Resources/Wiki/Anime/Theme/Schemas/ThemeForm.php b/app/Filament/Resources/Wiki/Anime/Theme/Schemas/ThemeForm.php new file mode 100644 index 000000000..3e860b569 --- /dev/null +++ b/app/Filament/Resources/Wiki/Anime/Theme/Schemas/ThemeForm.php @@ -0,0 +1,50 @@ +label(__('filament.fields.anime_theme.type.name')) + ->helperText(__('filament.fields.anime_theme.type.help')) + ->options(ThemeType::class) + ->required() + ->live() + ->partiallyRenderComponentsAfterStateUpdated([AnimeTheme::ATTRIBUTE_SLUG]) + ->afterStateUpdated(fn (Set $set, Get $get) => Theme::setThemeSlug($set, $get)); + } + + public static function sequenceField(): TextInput + { + return TextInput::make(AnimeTheme::ATTRIBUTE_SEQUENCE) + ->label(__('filament.fields.anime_theme.sequence.name')) + ->helperText(__('filament.fields.anime_theme.sequence.help')) + ->integer() + ->live() + ->partiallyRenderComponentsAfterStateUpdated([AnimeTheme::ATTRIBUTE_SLUG]) + ->afterStateUpdated(fn (Set $set, Get $get) => Theme::setThemeSlug($set, $get)); + } + + public static function slugField(): TextInput + { + return TextInput::make(AnimeTheme::ATTRIBUTE_SLUG) + ->label(__('filament.fields.anime_theme.slug.name')) + ->helperText(__('filament.fields.anime_theme.slug.help')) + ->required() + ->maxLength(192) + ->alphaDash() + ->readOnly(); + } +} diff --git a/app/Filament/Submission/Resources/Anime/Pages/CreateAnimeSubmission.php b/app/Filament/Submission/Resources/Anime/Pages/CreateAnimeSubmission.php new file mode 100644 index 000000000..1f2fb03cd --- /dev/null +++ b/app/Filament/Submission/Resources/Anime/Pages/CreateAnimeSubmission.php @@ -0,0 +1,307 @@ +components([ + Tabs::make('tabs') + ->vertical() + ->columnSpanFull() + ->tabs([ + Tab::make('anime') + ->statePath('anime') + ->label(__('filament.resources.label.anime')) + ->columns(2) + ->schema(AnimeModel::form($schema)->getComponents()), + + Tab::make(Anime::RELATION_SYNONYMS) + ->label(__('filament.resources.label.anime_synonyms')) + ->schema([ + Repeater::make(Anime::RELATION_SYNONYMS) + ->label(__('filament.resources.label.anime_synonyms')) + ->addActionLabel(__('filament.buttons.add', ['label' => __('filament.resources.singularLabel.anime_synonym')])) + ->defaultItems(0) + ->schema(Synonym::form($schema)->getComponents()), + ]), + + Tab::make(Anime::RELATION_THEMES) + ->label(__('filament.resources.label.anime_themes')) + ->schema([ + Repeater::make(Anime::RELATION_THEMES) + ->label(__('filament.resources.label.anime_themes')) + ->addActionLabel(__('filament.buttons.add', ['label' => __('filament.resources.singularLabel.anime_theme')])) + ->defaultItems(0) + ->schema([ + Tabs::make('Tabs') + ->schema([ + Tab::make('theme') + ->label(__('filament.resources.singularLabel.anime_theme')) + ->schema([ + ThemeForm::typeField(), + ThemeForm::sequenceField(), + ThemeForm::slugField(), + + SubmissionBelongsTo::make(AnimeTheme::ATTRIBUTE_GROUP) + ->resource(Group::class) + ->hiddenField('group_fields') + ->showCreateOption() + ->live() + ->partiallyRenderComponentsAfterStateUpdated([AnimeTheme::ATTRIBUTE_SLUG]) + ->afterStateUpdated(fn (Set $set, Get $get) => Theme::setThemeSlug($set, $get)), + + Hidden::make('group_fields') + ->default([]) + ->dehydrated(false), + ]), + + Tab::make('song') + ->label(__('filament.resources.singularLabel.song')) + ->schema([ + SubmissionBelongsTo::make(AnimeTheme::ATTRIBUTE_SONG) + ->resource(Song::class) + ->hiddenField('song_fields') + ->showCreateOption() + ->live() + ->hintAction(LoadArtistsAction::make()), + + Hidden::make('song_fields') + ->default([]) + ->dehydrated(false), + + Repeater::make('performances') + ->label(__('filament.resources.label.artists')) + ->addActionLabel(__('filament.buttons.add', ['label' => __('filament.resources.singularLabel.artist')])) + ->live(true) + ->key('song.performances') + ->collapsible() + ->defaultItems(0) + ->columns(3) + ->columnSpanFull() + ->reorderableWithDragAndDrop(false) + ->reorderableWithButtons() + ->formatStateUsing(function (Get $get): array { + /** @var WikiSong|null $song */ + $song = WikiSong::query()->find($get(Performance::ATTRIBUTE_SONG)); + + return PerformanceSongRelationManager::formatArtists($song); + }) + ->schema([ + SubmissionBelongsTo::make(Artist::ATTRIBUTE_ID) + ->resource(WikiArtist::class) + ->hiddenField('artist_fields') + ->showCreateOption() + ->required() + ->hintAction(LoadMembersAction::make()), + + Hidden::make('artist_fields') + ->default([]) + ->dehydrated(false), + + TextInput::make(Performance::ATTRIBUTE_AS) + ->label(__('filament.fields.performance.as.name')) + ->helperText(__('filament.fields.performance.as.help')), + + TextInput::make(Performance::ATTRIBUTE_ALIAS) + ->label(__('filament.fields.performance.alias.name')) + ->helperText(__('filament.fields.performance.alias.help')), + + Repeater::make('memberships') + ->label(__('filament.resources.label.memberships')) + ->helperText(__('filament.fields.performance.memberships.help')) + ->addActionLabel(__('filament.buttons.add', ['label' => __('filament.resources.singularLabel.member')])) + ->collapsible() + ->defaultItems(0) + ->columns(3) + ->columnSpanFull() + ->reorderableWithDragAndDrop(false) + ->reorderableWithButtons() + ->schema([ + SubmissionBelongsTo::make(Membership::ATTRIBUTE_MEMBER) + ->resource(WikiArtist::class) + ->hiddenField('member_fields') + ->showCreateOption() + ->label(__('filament.fields.membership.member')) + ->required(), + + Hidden::make('member_fields') + ->default([]) + ->dehydrated(false), + + TextInput::make(Membership::ATTRIBUTE_AS) + ->label(__('filament.fields.membership.as.name')) + ->helperText(__('filament.fields.membership.as.help')), + + TextInput::make(Membership::ATTRIBUTE_ALIAS) + ->label(__('filament.fields.membership.alias.name')) + ->helperText(__('filament.fields.membership.alias.help')), + ]), + ]), + ]), + + Tab::make('entries') + ->label(__('filament.resources.label.anime_theme_entries')) + ->schema([ + Repeater::make(AnimeTheme::RELATION_ENTRIES) + ->label(__('filament.resources.label.anime_theme_entries')) + ->addActionLabel(__('filament.buttons.add', ['label' => __('filament.resources.singularLabel.anime_theme_entry')])) + ->schema([ + TextInput::make(AnimeThemeEntry::ATTRIBUTE_VERSION) + ->label(__('filament.fields.anime_theme_entry.version.name')) + ->helperText(__('filament.fields.anime_theme_entry.version.help')) + ->integer(), + + TextInput::make(AnimeThemeEntry::ATTRIBUTE_EPISODES) + ->label(__('filament.fields.anime_theme_entry.episodes.name')) + ->helperText(__('filament.fields.anime_theme_entry.episodes.help')) + ->maxLength(192), + + Checkbox::make(AnimeThemeEntry::ATTRIBUTE_NSFW) + ->label(__('filament.fields.anime_theme_entry.nsfw.name')) + ->helperText(__('filament.fields.anime_theme_entry.nsfw.help')), + + Checkbox::make(AnimeThemeEntry::ATTRIBUTE_SPOILER) + ->label(__('filament.fields.anime_theme_entry.spoiler.name')) + ->helperText(__('filament.fields.anime_theme_entry.spoiler.help')), + + TextInput::make(AnimeThemeEntry::ATTRIBUTE_NOTES) + ->label(__('filament.fields.anime_theme_entry.notes.name')) + ->helperText(__('filament.fields.anime_theme_entry.notes.help')) + ->maxLength(192), + + TextInput::make(ResourceSite::YOUTUBE->name) + ->label(ResourceSite::YOUTUBE->localize()) + ->helperText(__('filament.fields.anime_theme_entry.youtube.help')) + ->url() + ->maxLength(255) + ->rule(new AnimeThemeEntryResourceLinkFormatRule(ResourceSite::YOUTUBE)) + ->uri(), + ]), + ]), + ]), + ]), + ]), + + Tab::make(Anime::RELATION_SERIES) + ->label(__('filament.resources.label.series')) + ->schema([ + Repeater::make(Anime::RELATION_SERIES) + ->label(__('filament.resources.label.series')) + ->addActionLabel(__('filament.buttons.add', ['label' => __('filament.resources.singularLabel.series')])) + ->defaultItems(0) + ->schema([ + SubmissionBelongsTo::make(Series::ATTRIBUTE_ID) + ->resource(SeriesResource::class) + ->showCreateOption(), + + Hidden::make('fields') + ->default([]) + ->dehydrated(false), + ]), + ]), + + Tab::make(Anime::RELATION_RESOURCES) + ->label(__('filament.resources.label.external_resources')) + ->schema([ + Repeater::make(Anime::RELATION_RESOURCES) + ->label(__('filament.resources.label.external_resources')) + ->addActionLabel(__('filament.buttons.add', ['label' => __('filament.resources.singularLabel.external_resource')])) + ->defaultItems(0) + ->schema([ + SubmissionBelongsTo::make(WikiExternalResource::ATTRIBUTE_ID) + ->resource(ExternalResource::class) + ->showCreateOption(), + + Hidden::make('fields') + ->default([]) + ->dehydrated(false), + ]), + ]), + + Tab::make(Anime::RELATION_STUDIOS) + ->label(__('filament.resources.label.studios')) + ->schema([ + Repeater::make(Anime::RELATION_STUDIOS) + ->label(__('filament.resources.label.studios')) + ->addActionLabel(__('filament.buttons.add', ['label' => __('filament.resources.singularLabel.studio')])) + ->defaultItems(0) + ->schema([ + SubmissionBelongsTo::make(Studio::ATTRIBUTE_ID) + ->resource(StudioResource::class) + ->showCreateOption(), + + Hidden::make('fields') + ->default([]) + ->dehydrated(false), + ]), + ]), + ]), + ]); + } + + protected function handleRecordCreation(array $data): Model + { + $this->halt(); + + return new Anime(); + } + + protected function getCreateFormAction(): Action + { + return parent::getCreateFormAction() + ->label('Submit'); + } + + protected function getCreateAnotherFormAction(): Action + { + return parent::getCreateAnotherFormAction() + ->label('Submit & Submit another'); + } +} diff --git a/app/Filament/Submission/Resources/Anime/Pages/ListAnimeSubmissions.php b/app/Filament/Submission/Resources/Anime/Pages/ListAnimeSubmissions.php new file mode 100644 index 000000000..76e8c1fad --- /dev/null +++ b/app/Filament/Submission/Resources/Anime/Pages/ListAnimeSubmissions.php @@ -0,0 +1,26 @@ +|null + */ + protected static ?string $model = AnimeModel::class; + + public static function getModelLabel(): string + { + return __('submissions.resources.singularLabel.anime'); + } + + public static function getPluralModelLabel(): string + { + return __('submissions.resources.label.anime'); + } + + public static function getNavigationIcon(): Heroicon + { + return Heroicon::OutlinedTv; + } + + public static function getRecordTitleAttribute(): string + { + return AnimeModel::ATTRIBUTE_NAME; + } + + public static function canGloballySearch(): bool + { + return true; + } + + public static function getRecordSlug(): string + { + return 'anime'; + } + + public static function table(Table $table): Table + { + return parent::table($table) + ->columns([ + TextColumn::make(AnimeModel::ATTRIBUTE_ID) + ->label(__('filament.fields.base.id')), + + TextColumn::make(AnimeModel::ATTRIBUTE_NAME) + ->label(__('filament.fields.anime.name.name')) + ->copyableWithMessage() + ->limit(50) + ->tooltip(fn (TextColumn $column): mixed => $column->getState()), + + TextColumn::make(AnimeModel::ATTRIBUTE_SLUG) + ->label(__('filament.fields.anime.slug.name')) + ->limit(20) + ->tooltip(fn (TextColumn $column): mixed => $column->getState()), + + TextColumn::make(AnimeModel::ATTRIBUTE_YEAR) + ->label(__('filament.fields.anime.year.name')), + + TextColumn::make(AnimeModel::ATTRIBUTE_SEASON) + ->label(__('filament.fields.anime.season.name')) + ->formatStateUsing(fn (AnimeSeason $state): string => $state->localizeStyled()) + ->html(), + + TextColumn::make(AnimeModel::ATTRIBUTE_MEDIA_FORMAT) + ->label(__('filament.fields.anime.media_format.name')) + ->formatStateUsing(fn (AnimeMediaFormat $state): ?string => $state->localize()), + ]) + ->searchable(); + } + + /** + * @return \Filament\Tables\Filters\BaseFilter[] + */ + public static function getFilters(): array + { + return [ + QueryBuilder::make() + ->constraints([ + TextConstraint::make(AnimeModel::ATTRIBUTE_NAME) + ->label(__('filament.fields.anime.name.name')), + + TextConstraint::make(AnimeModel::ATTRIBUTE_SLUG) + ->label(__('filament.fields.anime.slug.name')), + + NumberConstraint::make(AnimeModel::ATTRIBUTE_YEAR) + ->label(__('filament.fields.anime.year.name')), + + SelectConstraint::make(AnimeModel::ATTRIBUTE_SEASON) + ->label(__('filament.fields.anime.season.name')) + ->options(AnimeSeason::class) + ->multiple(), + + SelectConstraint::make(AnimeModel::ATTRIBUTE_MEDIA_FORMAT) + ->label(__('filament.fields.anime.media_format.name')) + ->options(AnimeMediaFormat::class) + ->multiple(), + + TextConstraint::make(AnimeModel::ATTRIBUTE_SYNOPSIS) + ->label(__('filament.fields.anime.synopsis.name')), + + ...parent::getConstraints(), + ]), + + ...parent::getFilters(), + ]; + } + + /** + * @return array + */ + public static function getPages(): array + { + return [ + 'index' => ListAnimeSubmissions::route('/'), + 'create' => CreateAnimeSubmission::route('/submit'), + ]; + } +} diff --git a/app/Filament/Submission/Resources/BaseSubmissionResource.php b/app/Filament/Submission/Resources/BaseSubmissionResource.php new file mode 100644 index 000000000..8420e729c --- /dev/null +++ b/app/Filament/Submission/Resources/BaseSubmissionResource.php @@ -0,0 +1,144 @@ +getKeyName(); + } + + public static function getRecordTitleAttribute(): string + { + return (new static::$model)->getKeyName(); + } + + public static function table(Table $table): Table + { + return $table + ->defaultSort(fn (Table $table): ?string => $table->hasSearch() ? null : static::getRecordRouteKeyName(), fn (Table $table): ?string => $table->hasSearch() ? null : 'desc') + ->filters(static::getFilters()) + ->filtersLayout(FiltersLayout::Modal) + ->filtersFormWidth(Width::FourExtraLarge) + ->filtersFormMaxHeight('400px') + ->recordActions(static::getActions()) + ->toolbarActions(static::getBulkActions()) + ->headerActions(static::getTableActions()) + ->recordUrl(fn (Model $record): string => static::getUrl('create', ['record' => $record])) + ->paginated([10, 25, 50, 100, 'all']) + ->defaultPaginationPageOption(25) + ->extremePaginationLinks() + ->deferLoading(! app()->runningUnitTests()); + } + + /** + * @return \Filament\Tables\Filters\BaseFilter[] + */ + public static function getFilters(): array + { + return [ + TrashedFilter::make() + ->visible(in_array(SoftDeletable::class, class_implements(static::getModel()))), + ]; + } + + /** + * @return Constraint[] + */ + public static function getConstraints(): array + { + return [ + DateConstraint::make(BaseModel::ATTRIBUTE_CREATED_AT) + ->label(__('filament.fields.base.created_at')), + + DateConstraint::make(BaseModel::ATTRIBUTE_UPDATED_AT) + ->label(__('filament.fields.base.updated_at')), + ]; + } + + /** + * @return array + */ + public static function getActions(): array + { + return [ + ...(Gate::allows('viewAny', static::$model) ? [ViewAction::make()] : []), + ]; + } + + /** + * @return Action[] + */ + public static function getRecordActions(): array + { + return []; + } + + /** + * @param array|null $actionsIncludedInGroup + * @return array + */ + public static function getBulkActions(?array $actionsIncludedInGroup = []): array + { + return []; + } + + /** + * @return array + */ + public static function getTableActions(): array + { + return []; + } + + public static function getEloquentQuery(): Builder + { + return parent::getEloquentQuery() + ->withoutGlobalScopes([ + SoftDeletingScope::class, + WithoutInsertSongScope::class, + ]); + } + + /** + * @return array> + */ + public static function getBaseRelations(): array + { + return []; + } + + public static function getSlug(?Panel $panel = null): string + { + return 'submissions/'.static::getRecordSlug(); + } + + abstract public static function getRecordSlug(): string; +} diff --git a/app/Http/Middleware/Models/Admin/UserExceedsSubmissionLimit.php b/app/Http/Middleware/Models/Admin/UserExceedsSubmissionLimit.php index fea04219f..970587c55 100644 --- a/app/Http/Middleware/Models/Admin/UserExceedsSubmissionLimit.php +++ b/app/Http/Middleware/Models/Admin/UserExceedsSubmissionLimit.php @@ -6,7 +6,7 @@ namespace App\Http\Middleware\Models\Admin; use App\Constants\Config\SubmissionConstants; use App\Enums\Auth\SpecialPermission; -use App\Enums\Models\User\ApprovableStatus; +use App\Enums\Models\User\SubmissionStatus; use App\Models\Auth\User; use App\Models\User\Submission; use Closure; @@ -25,7 +25,7 @@ class UserExceedsSubmissionLimit /** @var User|null $user */ $user = $request->user('sanctum'); - abort_if(intval($user?->submissions->where(Submission::ATTRIBUTE_STATUS, ApprovableStatus::PENDING->value)->count()) >= $submissionLimit + abort_if(intval($user?->submissions->where(Submission::ATTRIBUTE_STATUS, SubmissionStatus::PENDING->value)->count()) >= $submissionLimit && blank($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS->value)), 403, "User cannot have more than '$submissionLimit' outstanding submissions."); return $next($request); diff --git a/app/Models/Auth/User.php b/app/Models/Auth/User.php index aa02e2687..718e995a1 100644 --- a/app/Models/Auth/User.php +++ b/app/Models/Auth/User.php @@ -8,6 +8,7 @@ use App\Concerns\Models\SoftDeletes; use App\Contracts\Models\HasSubtitle; use App\Contracts\Models\Nameable; use App\Contracts\Models\SoftDeletable; +use App\Enums\Auth\Role; use App\Enums\Auth\SpecialPermission; use App\Enums\Http\Api\Filter\ComparisonOperator; use App\Events\Auth\User\UserCreated; @@ -174,6 +175,10 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, HasSubtit */ public function canAccessPanel(Panel $panel): bool { + if ($panel->getId() === 'submission') { + return $this->hasRole(Role::ADMIN->value); + } + if ($this->hasVerifiedEmail() && $this->hasAnyPermission(SpecialPermission::VIEW_FILAMENT->value)) { return true; } diff --git a/app/Models/User/Submission.php b/app/Models/User/Submission.php index 7b8263960..c2b84037f 100644 --- a/app/Models/User/Submission.php +++ b/app/Models/User/Submission.php @@ -4,27 +4,30 @@ declare(strict_types=1); namespace App\Models\User; -use App\Enums\Models\User\ApprovableStatus; +use App\Enums\Models\User\SubmissionStatus; use App\Models\Auth\User; use App\Models\BaseModel; -use App\Models\User\Submission\SubmissionStep; +use App\Models\User\Submission\SubmissionStage; use Database\Factories\User\SubmissionFactory; use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; /** + * @property string|null $actionable_type + * @property string|null $actionable_id * @property Carbon|null $finished_at + * @property bool $locked * @property User|null $moderator * @property int|null $moderator_id * @property string|null $moderator_notes - * @property string|null $notes - * @property ApprovableStatus $status - * @property Collection $steps + * @property SubmissionStatus $status + * @property Collection $stages * @property User|null $user * @property int|null $user_id * @@ -38,15 +41,19 @@ class Submission extends BaseModel final public const string TABLE = 'submissions'; final public const string ATTRIBUTE_ID = 'submission_id'; + final public const string ATTRIBUTE_ACTIONABLE_TYPE = 'actionable_type'; + final public const string ATTRIBUTE_ACTIONABLE_ID = 'actionable_id'; final public const string ATTRIBUTE_FINISHED_AT = 'finished_at'; + final public const string ATTRIBUTE_LOCKED = 'locked'; final public const string ATTRIBUTE_MODERATOR = 'moderator_id'; final public const string ATTRIBUTE_MODERATOR_NOTES = 'moderator_notes'; - final public const string ATTRIBUTE_NOTES = 'notes'; final public const string ATTRIBUTE_STATUS = 'status'; + final public const string ATTRIBUTE_TYPE = 'type'; final public const string ATTRIBUTE_USER = 'user_id'; + final public const string RELATION_ACTIONABLE = 'actionable'; final public const string RELATION_MODERATOR = 'moderator'; - final public const string RELATION_STEPS = 'steps'; + final public const string RELATION_STAGES = 'stages'; final public const string RELATION_USER = 'user'; /** @@ -62,11 +69,14 @@ class Submission extends BaseModel * @var list */ protected $fillable = [ + Submission::ATTRIBUTE_ACTIONABLE_TYPE, + Submission::ATTRIBUTE_ACTIONABLE_ID, Submission::ATTRIBUTE_FINISHED_AT, + Submission::ATTRIBUTE_LOCKED, Submission::ATTRIBUTE_MODERATOR, Submission::ATTRIBUTE_MODERATOR_NOTES, - Submission::ATTRIBUTE_NOTES, Submission::ATTRIBUTE_STATUS, + Submission::ATTRIBUTE_TYPE, Submission::ATTRIBUTE_USER, ]; @@ -107,25 +117,39 @@ class Submission extends BaseModel { return [ Submission::ATTRIBUTE_FINISHED_AT => 'datetime', - Submission::ATTRIBUTE_STATUS => ApprovableStatus::class, + Submission::ATTRIBUTE_LOCKED => 'bool', + Submission::ATTRIBUTE_STATUS => SubmissionStatus::class, ]; } + public function isLocked(): bool + { + return $this->locked; + } + /** * Scope a query to only include pending submissions. */ #[Scope] protected function pending(Builder $query): void { - $query->where(Submission::ATTRIBUTE_STATUS, ApprovableStatus::PENDING->value); + $query->where(Submission::ATTRIBUTE_STATUS, SubmissionStatus::PENDING->value); } /** - * @return HasMany + * Get the model that references the submission. */ - public function steps(): HasMany + public function actionable(): MorphTo { - return $this->hasMany(SubmissionStep::class, SubmissionStep::ATTRIBUTE_SUBMISSION); + return $this->morphTo(); + } + + /** + * @return HasMany + */ + public function stages(): HasMany + { + return $this->hasMany(SubmissionStage::class, SubmissionStage::ATTRIBUTE_SUBMISSION); } /** diff --git a/app/Models/User/Submission/SubmissionStage.php b/app/Models/User/Submission/SubmissionStage.php new file mode 100644 index 000000000..df9f5e66f --- /dev/null +++ b/app/Models/User/Submission/SubmissionStage.php @@ -0,0 +1,110 @@ + + */ + protected $fillable = [ + SubmissionStage::ATTRIBUTE_FIELDS, + SubmissionStage::ATTRIBUTE_MODERATOR, + SubmissionStage::ATTRIBUTE_MODERATOR_NOTES, + SubmissionStage::ATTRIBUTE_NOTES, + SubmissionStage::ATTRIBUTE_STAGE, + SubmissionStage::ATTRIBUTE_SUBMISSION, + ]; + + /** + * The table associated with the model. + * + * @var string + */ + protected $table = SubmissionStage::TABLE; + + /** + * The primary key associated with the table. + * + * @var string + */ + protected $primaryKey = SubmissionStage::ATTRIBUTE_ID; + + public function getName(): string + { + return strval($this->getKey()); + } + + public function getSubtitle(): string + { + return strval($this->getKey()); + } + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + SubmissionStage::ATTRIBUTE_FIELDS => 'array', + SubmissionStage::ATTRIBUTE_STAGE => 'int', + ]; + } + + /** + * @return BelongsTo + */ + public function submission(): BelongsTo + { + return $this->belongsTo(Submission::class, SubmissionStage::ATTRIBUTE_SUBMISSION); + } +} diff --git a/app/Models/User/Submission/SubmissionStep.php b/app/Models/User/Submission/SubmissionStep.php deleted file mode 100644 index c5a70c954..000000000 --- a/app/Models/User/Submission/SubmissionStep.php +++ /dev/null @@ -1,169 +0,0 @@ -|null $pivot - * @property Submission $submission - * @property ApprovableStatus $status - * @property Model|null $target - * @property string|null $target_type - * @property int|null $target_id - * - * @method static SubmissionStepFactory factory(...$parameters) - */ -class SubmissionStep extends BaseModel -{ - use HasFactory; - - final public const string TABLE = 'submission_step'; - - final public const string ATTRIBUTE_ID = 'step_id'; - - final public const string ATTRIBUTE_ACTION = 'action'; - final public const string ATTRIBUTE_ACTIONABLE_TYPE = 'actionable_type'; - final public const string ATTRIBUTE_ACTIONABLE_ID = 'actionable_id'; - - final public const string ATTRIBUTE_TARGET_TYPE = 'target_type'; - final public const string ATTRIBUTE_TARGET_ID = 'target_id'; - - final public const string ATTRIBUTE_PIVOT = 'pivot'; - - final public const string ATTRIBUTE_SUBMISSION = 'submission_id'; - final public const string ATTRIBUTE_FIELDS = 'fields'; - final public const string ATTRIBUTE_FINISHED_AT = 'finished_at'; - final public const string ATTRIBUTE_STATUS = 'status'; - - final public const string RELATION_ACTIONABLE = 'actionable'; - final public const string RELATION_TARGET = 'target'; - final public const string RELATION_SUBMISSION = 'submission'; - - /** - * Is auditing disabled? - * - * @var bool - */ - public static $auditingDisabled = true; - - /** - * The attributes that are mass assignable. - * - * @var list - */ - protected $fillable = [ - SubmissionStep::ATTRIBUTE_ACTION, - SubmissionStep::ATTRIBUTE_ACTIONABLE_TYPE, - SubmissionStep::ATTRIBUTE_ACTIONABLE_ID, - SubmissionStep::ATTRIBUTE_FIELDS, - SubmissionStep::ATTRIBUTE_PIVOT, - SubmissionStep::ATTRIBUTE_SUBMISSION, - SubmissionStep::ATTRIBUTE_STATUS, - SubmissionStep::ATTRIBUTE_TARGET_TYPE, - SubmissionStep::ATTRIBUTE_TARGET_ID, - ]; - - /** - * The table associated with the model. - * - * @var string - */ - protected $table = SubmissionStep::TABLE; - - /** - * The primary key associated with the table. - * - * @var string - */ - protected $primaryKey = SubmissionStep::ATTRIBUTE_ID; - - public function getName(): string - { - return strval($this->getKey()); - } - - public function getSubtitle(): string - { - return strval($this->getKey()); - } - - /** - * Get the attributes that should be cast. - * - * @return array - */ - protected function casts(): array - { - return [ - SubmissionStep::ATTRIBUTE_ACTION => SubmissionActionType::class, - SubmissionStep::ATTRIBUTE_FIELDS => 'array', - SubmissionStep::ATTRIBUTE_FINISHED_AT => 'datetime', - SubmissionStep::ATTRIBUTE_STATUS => ApprovableStatus::class, - ]; - } - - /** - * Format the fields to display in the admin panel - * TODO: Double check if this action can be refactored. - */ - public function formatFields(?array $fields = null): array - { - $newFields = []; - $actionable = $this->actionable; - - if ($fields === null) { - $fields = $this->fields; - } - - foreach ($fields as $column => $value) { - $casted = $actionable->hasCast($column) - ? $actionable->castAttribute($column, $value) - : $value; - - if (is_object($casted) && enum_exists($casted::class)) { - $newFields[$column] = $casted->localize(); - continue; - } - - $newFields[$column] = $casted; - } - - return $newFields; - } - - public function actionable(): MorphTo - { - return $this->morphTo(); - } - - public function target(): MorphTo - { - return $this->morphTo(); - } - - /** - * @return BelongsTo - */ - public function submission(): BelongsTo - { - return $this->belongsTo(Submission::class, SubmissionStep::ATTRIBUTE_SUBMISSION); - } -} diff --git a/app/Policies/User/Submission/SubmissionStepPolicy.php b/app/Policies/User/Submission/SubmissionStagePolicy.php similarity index 61% rename from app/Policies/User/Submission/SubmissionStepPolicy.php rename to app/Policies/User/Submission/SubmissionStagePolicy.php index da27660b0..9b2564ba7 100644 --- a/app/Policies/User/Submission/SubmissionStepPolicy.php +++ b/app/Policies/User/Submission/SubmissionStagePolicy.php @@ -7,13 +7,13 @@ namespace App\Policies\User\Submission; use App\Enums\Auth\CrudPermission; use App\Enums\Auth\Role; use App\Models\Auth\User; -use App\Models\User\Submission\SubmissionStep; +use App\Models\User\Submission\SubmissionStage; use App\Policies\BasePolicy; use Filament\Facades\Filament; use Illuminate\Auth\Access\Response; use Illuminate\Database\Eloquent\Model; -class SubmissionStepPolicy extends BasePolicy +class SubmissionStagePolicy extends BasePolicy { public function viewAny(?User $user, mixed $value = null): Response { @@ -27,9 +27,9 @@ class SubmissionStepPolicy extends BasePolicy } /** - * @param SubmissionStep $step + * @param SubmissionStage $stage */ - public function view(?User $user, Model $step): Response + public function view(?User $user, Model $stage): Response { if (Filament::isServing()) { return $user instanceof User && $user->hasRole(Role::ADMIN->value) @@ -37,29 +37,25 @@ class SubmissionStepPolicy extends BasePolicy : Response::deny(); } - return $step->submission->user()->is($user) && $user?->can(CrudPermission::VIEW->format(static::getModel())) + return $stage->submission->user()->is($user) && $user?->can(CrudPermission::VIEW->format(static::getModel())) ? Response::allow() : Response::deny(); } /** - * @param SubmissionStep $step + * @param SubmissionStage $stage */ - public function update(User $user, Model $step): Response + public function update(User $user, Model $stage): Response { - if ($user->hasRole(Role::ADMIN->value)) { - return Response::allow(); - } - - return $step->submission->user()->is($user) && $user->can(CrudPermission::UPDATE->format(static::getModel())) + return $user->hasRole(Role::ADMIN->value) ? Response::allow() : Response::deny(); } /** - * @param SubmissionStep $step + * @param SubmissionStage $stage */ - public function delete(User $user, Model $step): Response + public function delete(User $user, Model $stage): Response { return $user->hasRole(Role::ADMIN->value) ? Response::allow() diff --git a/app/Policies/User/SubmissionPolicy.php b/app/Policies/User/SubmissionPolicy.php index 71f691bf2..10365488b 100644 --- a/app/Policies/User/SubmissionPolicy.php +++ b/app/Policies/User/SubmissionPolicy.php @@ -51,7 +51,7 @@ class SubmissionPolicy extends BasePolicy return Response::allow(); } - return $submission->user()->is($user) && $user->can(CrudPermission::UPDATE->format(static::getModel())) + return ! $submission->isLocked() && $submission->user()->is($user) && $user->can(CrudPermission::UPDATE->format(static::getModel())) ? Response::allow() : Response::deny(); } diff --git a/app/Providers/Filament/SubmissionPanelProvider.php b/app/Providers/Filament/SubmissionPanelProvider.php new file mode 100644 index 000000000..e4c48d766 --- /dev/null +++ b/app/Providers/Filament/SubmissionPanelProvider.php @@ -0,0 +1,53 @@ +id('submission') + ->path('submission') + ->login() + ->maxContentWidth(Width::Full) + ->globalSearch(false) + ->colors([ + 'primary' => Color::Amber, + ]) + ->discoverResources(in: app_path('Filament/Submission/Resources'), for: 'App\Filament\Submission\Resources') + ->discoverPages(in: app_path('Filament/Submission/Pages'), for: 'App\Filament\Submission\Pages') + ->discoverWidgets(in: app_path('Filament/Submission/Widgets'), for: 'App\Filament\Submission\Widgets') + ->middleware([ + EncryptCookies::class, + AddQueuedCookiesToResponse::class, + StartSession::class, + AuthenticateSession::class, + ShareErrorsFromSession::class, + VerifyCsrfToken::class, + SubstituteBindings::class, + DisableBladeIconComponents::class, + DispatchServingFilamentEvent::class, + ]) + ->authMiddleware([ + Authenticate::class, + ]); + } +} diff --git a/config/app.php b/config/app.php index 9b5749d74..152a9bb1a 100644 --- a/config/app.php +++ b/config/app.php @@ -227,6 +227,7 @@ return [ App\Providers\GraphQLServiceProvider::class, App\Providers\HorizonServiceProvider::class, App\Providers\PulseServiceProvider::class, + App\Providers\Filament\SubmissionPanelProvider::class, App\Providers\RouteServiceProvider::class, ])->toArray(), diff --git a/database/factories/User/Submission/SubmissionStageFactory.php b/database/factories/User/Submission/SubmissionStageFactory.php new file mode 100644 index 000000000..d3eae66e3 --- /dev/null +++ b/database/factories/User/Submission/SubmissionStageFactory.php @@ -0,0 +1,38 @@ + + */ +class SubmissionStageFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var class-string + */ + protected $model = SubmissionStage::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + SubmissionStage::ATTRIBUTE_SUBMISSION => Submission::factory(), + SubmissionStage::ATTRIBUTE_MODERATOR_NOTES => fake()->text(), + ]; + } +} diff --git a/database/factories/User/Submission/SubmissionStepFactory.php b/database/factories/User/Submission/SubmissionStepFactory.php deleted file mode 100644 index f31620013..000000000 --- a/database/factories/User/Submission/SubmissionStepFactory.php +++ /dev/null @@ -1,45 +0,0 @@ - - */ -class SubmissionStepFactory extends Factory -{ - /** - * The name of the factory's corresponding model. - * - * @var class-string - */ - protected $model = SubmissionStep::class; - - /** - * Define the model's default state. - * - * @return array - */ - public function definition(): array - { - $action = Arr::random(SubmissionActionType::cases()); - $status = Arr::random(ApprovableStatus::cases()); - - return [ - SubmissionStep::ATTRIBUTE_ACTION => $action->value, - SubmissionStep::ATTRIBUTE_STATUS => $status->value, - SubmissionStep::ATTRIBUTE_SUBMISSION => Submission::factory(), - ]; - } -} diff --git a/database/factories/User/SubmissionFactory.php b/database/factories/User/SubmissionFactory.php index 8a1749921..f6bee4934 100644 --- a/database/factories/User/SubmissionFactory.php +++ b/database/factories/User/SubmissionFactory.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Database\Factories\User; -use App\Enums\Models\User\ApprovableStatus; +use App\Enums\Models\User\SubmissionStatus; use App\Models\User\Submission; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Arr; @@ -31,7 +31,7 @@ class SubmissionFactory extends Factory */ public function definition(): array { - $status = Arr::random(ApprovableStatus::cases()); + $status = Arr::random(SubmissionStatus::cases()); return [ Submission::ATTRIBUTE_MODERATOR_NOTES => fake()->text(), diff --git a/database/migrations/2025_11_28_195436_create_submissions_table.php b/database/migrations/2025_12_09_141202_create_submissions_table.php similarity index 55% rename from database/migrations/2025_11_28_195436_create_submissions_table.php rename to database/migrations/2025_12_09_141202_create_submissions_table.php index 8d149dc64..8afb3c3db 100644 --- a/database/migrations/2025_11_28_195436_create_submissions_table.php +++ b/database/migrations/2025_12_09_141202_create_submissions_table.php @@ -4,7 +4,7 @@ declare(strict_types=1); use App\Models\Auth\User; use App\Models\User\Submission; -use App\Models\User\Submission\SubmissionStep; +use App\Models\User\Submission\SubmissionStage; use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; @@ -20,7 +20,8 @@ return new class extends Migration Schema::create(Submission::TABLE, function (Blueprint $table) { $table->id(Submission::ATTRIBUTE_ID); - $table->longText(Submission::ATTRIBUTE_NOTES)->nullable(); + $table->nullableUuidMorphs(Submission::RELATION_ACTIONABLE); + $table->string(Submission::ATTRIBUTE_TYPE); $table->longText(Submission::ATTRIBUTE_MODERATOR_NOTES)->nullable(); $table->integer(Submission::ATTRIBUTE_STATUS)->nullable(); @@ -31,6 +32,7 @@ return new class extends Migration $table->unsignedBigInteger(Submission::ATTRIBUTE_MODERATOR)->nullable(); $table->foreign(Submission::ATTRIBUTE_MODERATOR)->references(User::ATTRIBUTE_ID)->on(User::TABLE)->nullOnDelete(); + $table->boolean(Submission::ATTRIBUTE_LOCKED)->default(false); $table->timestamp(Submission::ATTRIBUTE_FINISHED_AT, 6)->nullable(); $table->timestamps(6); @@ -38,22 +40,22 @@ return new class extends Migration }); } - if (! Schema::hasTable(SubmissionStep::TABLE)) { - Schema::create(SubmissionStep::TABLE, function (Blueprint $table) { - $table->id(SubmissionStep::ATTRIBUTE_ID); + if (! Schema::hasTable(SubmissionStage::TABLE)) { + Schema::create(SubmissionStage::TABLE, function (Blueprint $table) { + $table->id(SubmissionStage::ATTRIBUTE_ID); - $table->integer(SubmissionStep::ATTRIBUTE_ACTION)->nullable(); - $table->nullableMorphs(SubmissionStep::RELATION_ACTIONABLE); - $table->nullableMorphs(SubmissionStep::RELATION_TARGET); - $table->string(SubmissionStep::ATTRIBUTE_PIVOT)->nullable(); + $table->integer(SubmissionStage::ATTRIBUTE_STAGE); - $table->json(SubmissionStep::ATTRIBUTE_FIELDS)->nullable(); - $table->integer(SubmissionStep::ATTRIBUTE_STATUS)->nullable(); + $table->json(SubmissionStage::ATTRIBUTE_FIELDS)->nullable(); + $table->longText(SubmissionStage::ATTRIBUTE_NOTES)->nullable(); + $table->longText(SubmissionStage::ATTRIBUTE_MODERATOR_NOTES)->nullable(); - $table->unsignedBigInteger(SubmissionStep::ATTRIBUTE_SUBMISSION); - $table->foreign(SubmissionStep::ATTRIBUTE_SUBMISSION)->references(Submission::ATTRIBUTE_ID)->on(Submission::TABLE)->cascadeOnDelete(); + $table->unsignedBigInteger(SubmissionStage::ATTRIBUTE_SUBMISSION); + $table->foreign(SubmissionStage::ATTRIBUTE_SUBMISSION)->references(Submission::ATTRIBUTE_ID)->on(Submission::TABLE)->cascadeOnDelete(); + + $table->unsignedBigInteger(SubmissionStage::ATTRIBUTE_MODERATOR)->nullable(); + $table->foreign(SubmissionStage::ATTRIBUTE_MODERATOR)->references(User::ATTRIBUTE_ID)->on(User::TABLE)->nullOnDelete(); - $table->timestamp(SubmissionStep::ATTRIBUTE_FINISHED_AT, 6)->nullable(); $table->timestamps(6); }); } @@ -64,7 +66,7 @@ return new class extends Migration */ public function down(): void { - Schema::dropIfExists(SubmissionStep::TABLE); + Schema::dropIfExists(SubmissionStage::TABLE); Schema::dropIfExists(Submission::TABLE); } }; diff --git a/lang/en/enums.php b/lang/en/enums.php index 4bb33ca5b..3881f3c46 100644 --- a/lang/en/enums.php +++ b/lang/en/enums.php @@ -7,8 +7,7 @@ use App\Enums\Models\List\ExternalEntryWatchStatus; use App\Enums\Models\List\ExternalProfileSite; use App\Enums\Models\List\ExternalProfileVisibility; use App\Enums\Models\List\PlaylistVisibility; -use App\Enums\Models\User\ApprovableStatus; -use App\Enums\Models\User\SubmissionActionType; +use App\Enums\Models\User\SubmissionStatus; use App\Enums\Models\Wiki\AnimeMediaFormat; use App\Enums\Models\Wiki\AnimeSeason; use App\Enums\Models\Wiki\AnimeSynonymType; @@ -45,12 +44,6 @@ return [ AnimeSynonymType::ENGLISH->name => 'English', AnimeSynonymType::SHORT->name => 'Short', ], - ApprovableStatus::class => [ - ApprovableStatus::PENDING->name => 'Pending', - ApprovableStatus::REJECTED->name => 'Rejected', - ApprovableStatus::PARTIALLY_APPROVED->name => 'Partially Approved', - ApprovableStatus::APPROVED->name => 'Approved', - ], ExternalEntryWatchStatus::class => [ ExternalEntryWatchStatus::WATCHING->name => 'Watching', ExternalEntryWatchStatus::COMPLETED->name => 'Completed', @@ -81,12 +74,12 @@ return [ PlaylistVisibility::PRIVATE->name => 'Private', PlaylistVisibility::UNLISTED->name => 'Unlisted', ], - SubmissionActionType::class => [ - SubmissionActionType::CREATE->name => 'Create', - SubmissionActionType::UPDATE->name => 'Update', - SubmissionActionType::DELETE->name => 'Delete', - SubmissionActionType::ATTACH->name => 'Attach', - SubmissionActionType::DETACH->name => 'Detach', + SubmissionStatus::class => [ + SubmissionStatus::PENDING->name => 'Pending', + SubmissionStatus::CHANGES_REQUESTED->name => 'Changes Requested', + SubmissionStatus::REJECTED->name => 'Rejected', + SubmissionStatus::PARTIALLY_APPROVED->name => 'Partially Approved', + SubmissionStatus::APPROVED->name => 'Approved', ], ResourceSite::class => [ ResourceSite::OFFICIAL_SITE->name => 'Official Website', diff --git a/lang/en/filament.php b/lang/en/filament.php index 88d702635..2aef41bbf 100644 --- a/lang/en/filament.php +++ b/lang/en/filament.php @@ -848,25 +848,16 @@ return [ ], 'submission' => [ 'finished_at' => 'Finished At', + 'locked' => 'Locked', + 'moderator' => 'Moderator', + 'moderator_notes' => 'Moderator Notes', + 'status' => 'Status', + ], + 'submission_stage' => [ + 'stage' => 'Stage', 'moderator' => 'Moderator', 'moderator_notes' => 'Moderator Notes', 'notes' => 'Notes', - 'status' => 'Status', - 'target' => 'Target', - ], - 'submission_step' => [ - 'action' => 'Action', - 'actionable' => 'Actionable', - 'fields' => [ - 'name' => 'Fields', - 'columns' => 'Columns', - 'old_values' => 'Old Values', - 'values' => 'Values', - ], - 'finished_at' => 'Finished At', - 'pivot' => 'Pivot', - 'status' => 'Status', - 'target' => 'Target', ], 'resourceable' => [ 'as' => [ @@ -1013,7 +1004,7 @@ return [ 'playlist_tracks' => 'Playlist Tracks', 'playlists' => 'Playlists', 'submissions' => 'Submissions', - 'submission_steps' => 'Submission Steps', + 'submission_stages' => 'Submission Stages', 'roles' => 'Roles', 'series' => 'Series', 'songs' => 'Songs', @@ -1049,7 +1040,7 @@ return [ 'playlist_track' => 'Playlist Track', 'playlist' => 'Playlist', 'submission' => 'Submission', - 'submission_step' => 'Submission Step', + 'submission_stage' => 'Submission Stage', 'role' => 'Role', 'series' => 'Series', 'song' => 'Song', diff --git a/lang/en/submissions.php b/lang/en/submissions.php new file mode 100644 index 000000000..2fd78077d --- /dev/null +++ b/lang/en/submissions.php @@ -0,0 +1,14 @@ + [ + 'label' => [ + 'anime' => 'Anime', + ], + 'singularLabel' => [ + 'anime' => 'Anime Submission', + ], + ], +]; diff --git a/tests/Unit/Models/User/Submission/SubmissionStageTest.php b/tests/Unit/Models/User/Submission/SubmissionStageTest.php new file mode 100644 index 000000000..63f080eb6 --- /dev/null +++ b/tests/Unit/Models/User/Submission/SubmissionStageTest.php @@ -0,0 +1,45 @@ +for(Submission::factory()) + ->createOne(); + + $this->assertIsString($stage->getName()); +}); + +test('has subtitle', function () { + $stage = SubmissionStage::factory() + ->for(Submission::factory()) + ->createOne(); + + $this->assertIsString($stage->getSubtitle()); +}); + +test('casts fields to array', function () { + $anime = Anime::factory()->makeOne(); + + $stage = SubmissionStage::factory() + ->for(Submission::factory()) + ->createOne([SubmissionStage::ATTRIBUTE_FIELDS => $anime->attributesToArray()]); + + $this->assertIsArray($stage->fields); +}); + +test('submission', function () { + $stage = SubmissionStage::factory() + ->for(Submission::factory()) + ->createOne(); + + $this->assertInstanceOf(BelongsTo::class, $stage->submission()); + $this->assertInstanceOf(Submission::class, $stage->submission()->first()); +}); diff --git a/tests/Unit/Models/User/Submission/SubmissionStepTest.php b/tests/Unit/Models/User/Submission/SubmissionStepTest.php deleted file mode 100644 index 8e11d5a67..000000000 --- a/tests/Unit/Models/User/Submission/SubmissionStepTest.php +++ /dev/null @@ -1,72 +0,0 @@ -for(Submission::factory()) - ->createOne(); - - $this->assertIsString($step->getName()); -}); - -test('has subtitle', function () { - $step = SubmissionStep::factory() - ->for(Submission::factory()) - ->createOne(); - - $this->assertIsString($step->getSubtitle()); -}); - -test('casts action to enum', function () { - $step = SubmissionStep::factory() - ->for(Submission::factory()) - ->createOne(); - - $this->assertInstanceOf(SubmissionActionType::class, $step->action); -}); - -test('casts fields to array', function () { - $anime = Anime::factory()->makeOne(); - - $step = SubmissionStep::factory() - ->for(Submission::factory()) - ->createOne([SubmissionStep::ATTRIBUTE_FIELDS => $anime->attributesToArray()]); - - $this->assertIsArray($step->fields); -}); - -test('casts finished at', function () { - $step = SubmissionStep::factory() - ->for(Submission::factory()) - ->createOne([SubmissionStep::ATTRIBUTE_FINISHED_AT => now()]); - - $this->assertInstanceOf(Carbon::class, $step->finished_at); -}); - -test('casts status to enum', function () { - $step = SubmissionStep::factory() - ->for(Submission::factory()) - ->createOne(); - - $this->assertInstanceOf(ApprovableStatus::class, $step->status); -}); - -test('submission', function () { - $step = SubmissionStep::factory() - ->for(Submission::factory()) - ->createOne(); - - $this->assertInstanceOf(BelongsTo::class, $step->submission()); - $this->assertInstanceOf(Submission::class, $step->submission()->first()); -}); diff --git a/tests/Unit/Models/User/SubmissionTest.php b/tests/Unit/Models/User/SubmissionTest.php index a9e058bc8..e610d07fc 100644 --- a/tests/Unit/Models/User/SubmissionTest.php +++ b/tests/Unit/Models/User/SubmissionTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -use App\Enums\Models\User\ApprovableStatus; +use App\Enums\Models\User\SubmissionStatus; use App\Models\Auth\User; use App\Models\User\Submission; -use App\Models\User\Submission\SubmissionStep; +use App\Models\User\Submission\SubmissionStage; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Carbon; @@ -33,22 +33,22 @@ test('casts finished at', function () { test('casts status to enum', function () { $submission = Submission::factory()->createOne(); - $this->assertInstanceOf(ApprovableStatus::class, $submission->status); + $this->assertInstanceOf(SubmissionStatus::class, $submission->status); }); -test('steps', function () { - $stepsCount = fake()->randomDigitNotNull(); +test('stages', function () { + $stagesCount = fake()->randomDigitNotNull(); $submission = Submission::factory()->createOne(); - SubmissionStep::factory() + SubmissionStage::factory() ->for($submission) - ->count($stepsCount) + ->count($stagesCount) ->create(); - $this->assertInstanceOf(HasMany::class, $submission->steps()); - $this->assertEquals($stepsCount, $submission->steps()->count()); - $this->assertInstanceOf(SubmissionStep::class, $submission->steps()->first()); + $this->assertInstanceOf(HasMany::class, $submission->stages()); + $this->assertEquals($stagesCount, $submission->stages()->count()); + $this->assertInstanceOf(SubmissionStage::class, $submission->stages()->first()); }); test('user', function () {