diff --git a/app/Filament/Resources/PostResource.php b/app/Filament/Resources/PostResource.php index 1660f6d6..2334bf00 100644 --- a/app/Filament/Resources/PostResource.php +++ b/app/Filament/Resources/PostResource.php @@ -12,14 +12,18 @@ use Filament\Forms\Components\Select; use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; -use Filament\Forms\Components\Toggle; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables\Actions\BulkActionGroup; use Filament\Tables\Actions\DeleteBulkAction; use Filament\Tables\Actions\EditAction; use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Columns\IconColumn; use Filament\Tables\Table; +use Filament\Tables\Filters\Filter; +use Filament\Tables\Filters\SelectFilter; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Validation\Rules\Unique; final class PostResource extends Resource { @@ -37,53 +41,70 @@ public static function form(Form $form): Form ->maxLength(self::INPUT_MAX_LENGTH), TextInput::make('slug') ->required() - ->maxLength(self::INPUT_MAX_LENGTH), - TextInput::make('legacy_id') - ->numeric(), + ->maxLength(self::INPUT_MAX_LENGTH) + ->unique(Post::class, 'slug', ignoreRecord: true, modifyRuleUsing: fn(Unique $rule) => $rule->where('is_published', true)), Textarea::make('body') ->required() ->columnSpanFull(), Textarea::make('more_inside') ->columnSpanFull(), - TextInput::make('state') - ->required() - ->maxLength(self::INPUT_MAX_LENGTH), Select::make('subsite_id') ->relationship('subsite', 'name') ->required(), Select::make('user_id') ->relationship('user', 'name') + ->searchable() + ->preload() ->required(), - TextInput::make('uuid') - ->label('UUID') - ->maxLength(36), DateTimePicker::make('published_at'), - Toggle::make('is_published') - ->required(), - Toggle::make('is_current') - ->required(), - TextInput::make('publisher_type') - ->maxLength(self::INPUT_MAX_LENGTH), - TextInput::make('publisher_id') - ->numeric(), ]); } + public static function getEloquentQuery(): Builder + { + return Post::current(); + } + public static function table(Table $table): Table { return $table ->columns([ TextColumn::make('title') - ->searchable(), + ->searchable() + ->sortable() + ->limit(50), + TextColumn::make('slug') + ->searchable() + ->toggleable(isToggledHiddenByDefault: true), + IconColumn::make('is_published') + ->boolean() + ->label('Published') + ->sortable(), TextColumn::make('subsite.name') - ->numeric() + ->label('Subsite') ->sortable(), TextColumn::make('user.name') - ->numeric() + ->label('Author') + ->searchable() ->sortable(), + TextColumn::make('published_at') + ->dateTime() + ->sortable() + ->toggleable(), + TextColumn::make('created_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ - // + Filter::make('hide_drafts') + ->label('Hide drafts') + ->baseQuery(fn(Builder $query): Builder => $query->withoutDrafts()) + ->default(false), + SelectFilter::make('subsite_id') + ->relationship('subsite', 'name') + ->searchable() + ->preload(), ]) ->actions([ EditAction::make(), diff --git a/app/Filament/Resources/PostResource/Pages/EditPost.php b/app/Filament/Resources/PostResource/Pages/EditPost.php index f012f9da..21621ce6 100644 --- a/app/Filament/Resources/PostResource/Pages/EditPost.php +++ b/app/Filament/Resources/PostResource/Pages/EditPost.php @@ -5,6 +5,8 @@ namespace App\Filament\Resources\PostResource\Pages; use App\Filament\Resources\PostResource; +use App\Models\Post; +use Filament\Actions\Action; use Filament\Actions\DeleteAction; use Filament\Resources\Pages\EditRecord; @@ -16,6 +18,27 @@ protected function getHeaderActions(): array { return [ DeleteAction::make(), + Action::make('publish') + ->label('Publish') + ->visible(fn(Post $record) => $record->is_published === false) + ->action(function (Post $record) { + $record->setLive(); + $record->save(); + }) + ->after(function () { + $this->refreshFormData(['is_published', 'published_at']); + }), + Action::make('unpublish') + ->label('Unpublish') + ->visible(fn(Post $record) => $record->is_published === true) + ->action(function (Post $record) { + $record->is_published = false; + $record->save(); + }) + ->after(function () { + $this->refreshFormData(['is_published', 'published_at']); + }), + ]; } } diff --git a/app/Filament/Resources/SnippetResource.php b/app/Filament/Resources/SnippetResource.php index f2ed9614..9928d058 100644 --- a/app/Filament/Resources/SnippetResource.php +++ b/app/Filament/Resources/SnippetResource.php @@ -9,6 +9,8 @@ use App\Filament\Resources\SnippetsResource\Pages\ListSnippets; use App\Models\Snippet; use Filament\Forms\Form; +use Filament\Forms\Components\RichEditor; +use Filament\Forms\Components\TextInput; use Filament\Resources\Resource; use Filament\Tables\Actions\BulkActionGroup; use Filament\Tables\Actions\DeleteBulkAction; @@ -21,11 +23,18 @@ final class SnippetResource extends Resource protected static ?string $model = Snippet::class; protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack'; + public const int INPUT_MAX_LENGTH = 255; + public static function form(Form $form): Form { return $form ->schema([ - // + TextInput::make('title') + ->required() + ->maxLength(self::INPUT_MAX_LENGTH), + RichEditor::make('body') + ->required() + ->columnSpanFull(), ]); } diff --git a/app/Filament/Resources/SnippetsResource/Pages/EditSnippets.php b/app/Filament/Resources/SnippetsResource/Pages/EditSnippets.php index 6ae09c0e..0091d49c 100644 --- a/app/Filament/Resources/SnippetsResource/Pages/EditSnippets.php +++ b/app/Filament/Resources/SnippetsResource/Pages/EditSnippets.php @@ -6,7 +6,7 @@ use App\Filament\Resources\SnippetResource; use Filament\Resources\Pages\EditRecord; -use Filament\Tables\Actions\DeleteAction; +use Filament\Actions\DeleteAction; final class EditSnippets extends EditRecord { diff --git a/app/Livewire/Contact/ContactMessageForm.php b/app/Livewire/Contact/ContactMessageForm.php index 1e4244a2..3ce68ae7 100644 --- a/app/Livewire/Contact/ContactMessageForm.php +++ b/app/Livewire/Contact/ContactMessageForm.php @@ -1,5 +1,3 @@ - PostPresenter::class, ]; + protected $attributes = [ + 'state' => PostStateEnum::Draft->value, + ]; + public function toSearchableArray(): array { return array_merge($this->toArray(), [ @@ -135,17 +143,17 @@ public function comments(): HasMany return $this->hasMany(Comment::class); } - public function bookmarks(): int + public function bookmarkCount(): int { return Bookmark::count($this); } - public function favorites(): int + public function favoriteCount(): int { return Favorite::count($this); } - public function flags(): int + public function flagCount(): int { return Flag::count($this); } diff --git a/app/Policies/PostPolicy.php b/app/Policies/PostPolicy.php index 546e5dad..a0456370 100644 --- a/app/Policies/PostPolicy.php +++ b/app/Policies/PostPolicy.php @@ -4,6 +4,7 @@ namespace App\Policies; +use App\Enums\RoleNameEnum; use App\Models\User; use App\Models\Post; use Illuminate\Auth\Access\HandlesAuthorization; @@ -19,7 +20,7 @@ public function viewAny(): bool public function view(User $user, Post $post): bool { - if ($user->hasRole(['admin']) || $post->user_id === $user->id) { + if ($user->hasRole([RoleNameEnum::MODERATOR->value]) || $post->user_id === $user->id) { return true; } @@ -34,7 +35,7 @@ public function create(User $user): bool public function update(User $user, Post $post): bool { - if ($user->hasRole(['admin']) || $post->user_id === $user->id) { + if ($user->hasRole([RoleNameEnum::MODERATOR->value]) || $post->user_id === $user->id) { return true; } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 4d85ed9b..85c9f88d 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -34,7 +34,10 @@ public function boot(): void Model::shouldBeStrict(); Relation::morphMap([ + 'admin_notes' => 'App\Models\AdminNote', + 'comments' => 'App\Models\Comment', 'posts' => 'App\Models\Post', + 'snippets' => 'App\Models\Snippet', 'users' => 'App\Models\User', ]); diff --git a/database/migrations/2025_06_19_180714_update_posts_slug_unique_constraint_to_partial.php b/database/migrations/2025_06_19_180714_update_posts_slug_unique_constraint_to_partial.php new file mode 100644 index 00000000..d76b70ca --- /dev/null +++ b/database/migrations/2025_06_19_180714_update_posts_slug_unique_constraint_to_partial.php @@ -0,0 +1,32 @@ +dropUnique(['slug']); + }); + + // Uses IF(is_published = 1, 1, NULL) so only published records enforce uniqueness. + DB::statement('CREATE UNIQUE INDEX posts_slug_published_unique ON posts (slug, (IF(is_published = 1, 1, NULL)))'); + } + + public function down(): void + { + // Drop the functional unique index + DB::statement('DROP INDEX posts_slug_published_unique ON posts'); + + Schema::table('posts', function (Blueprint $table) { + // Restore the original unique constraint + $table->unique('slug'); + }); + } +}; diff --git a/resources/views/livewire/posts/post-comments-component.blade.php b/resources/views/livewire/posts/post-comments-component.blade.php index 6b0c91ac..987bafc4 100644 --- a/resources/views/livewire/posts/post-comments-component.blade.php +++ b/resources/views/livewire/posts/post-comments-component.blade.php @@ -6,8 +6,8 @@ @include('comments.partials.comment-footer', [ 'comment' => $comment, - 'favoritesCount' => $comment->favorites()->count(), - 'flagsCount' => $comment->flags()->count(), + 'favoritesCount' => $comment->favoriteCount(), + 'flagsCount' => $comment->flagCount(), 'flagReasons' => $flagReasons, ]) diff --git a/resources/views/posts/show.blade.php b/resources/views/posts/show.blade.php index f40f1ccd..9ba8b775 100644 --- a/resources/views/posts/show.blade.php +++ b/resources/views/posts/show.blade.php @@ -43,7 +43,7 @@ @include('posts.partials.post-show-footer', [ 'post' => $post, 'commentsCount' => $post->comments()->count(), - 'favoritesCount' => $post->favorites(), + 'favoritesCount' => $post->favoriteCount(), ])