Skip to content

Commit

Permalink
feat: articles crud (#134)
Browse files Browse the repository at this point in the history
  • Loading branch information
alfonsobries authored Sep 26, 2023
1 parent d723bea commit ecd3f75
Show file tree
Hide file tree
Showing 17 changed files with 511 additions and 10 deletions.
10 changes: 10 additions & 0 deletions app/Enums/ArticleCategoryEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace App\Enums;

enum ArticleCategoryEnum: string
{
case News = 'news';
}
2 changes: 2 additions & 0 deletions app/Enums/Role.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ enum Role: string
{
case Superadmin = 'superadmin';
case Admin = 'admin';
case Editor = 'editor';

public function label(): string
{
return match ($this) {
self::Superadmin => 'Super Administrator',
self::Admin => 'Administrator',
self::Editor => 'Editor',
};
}
}
126 changes: 126 additions & 0 deletions app/Filament/Resources/ArticleResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

declare(strict_types=1);

namespace App\Filament\Resources;

use App\Enums\ArticleCategoryEnum;
use App\Filament\Resources\ArticleResource\Pages\CreateArticle;
use App\Filament\Resources\ArticleResource\Pages\EditArticle;
use App\Filament\Resources\ArticleResource\Pages\ListArticles;
use App\Filament\Resources\ArticleResource\Pages\ViewArticle;
use App\Models\Article;
use App\Models\User;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Support\Str;

class ArticleResource extends Resource
{
protected static ?string $model = Article::class;

protected static ?string $navigationIcon = 'heroicon-o-document-text';

public static function form(Form $form): Form
{
return $form
->schema([
TextInput::make('title')->required()->columnSpan('full'),
Select::make('category')
->options([
ArticleCategoryEnum::News->value => Str::title(ArticleCategoryEnum::News->value),
])
->default(ArticleCategoryEnum::News->value)
->required(),
Textarea::make('meta_description')->nullable()->autosize()->columnSpan('full'),
Textarea::make('content')->required()->autosize()->columnSpan('full'),
Select::make('user_id')
->relationship(
name: 'user',
modifyQueryUsing: fn ($query) => $query->managers()->orderBy('username')->orderBy('email')
)
->getOptionLabelFromRecordUsing(fn (User $user) => $user->username ?? $user->email ?? 'ID '.$user->id)
->required(),
DatePicker::make('published_at')->nullable(),
]);
}

public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('title')
->label('Title')
->sortable()
->searchable(),
TextColumn::make('category')
->label('Category')
->sortable()
->searchable(),

TextColumn::make('published_at')
->label('Date Published')
->date()
->sortable(),

TextColumn::make('created_at')
->label('Date Created')
->dateTime()
->sortable(),

])
->filters([
//
])
->recordUrl(fn (Article $article) => ArticleResource::getUrl('view', ['record' => $article]))
->actions([
ViewAction::make(),
])
->emptyStateActions([
CreateAction::make(),
]);
}

public static function getRelations(): array
{
return [
//
];
}

public static function getPages(): array
{
return [
'index' => ListArticles::route('/'),
'create' => CreateArticle::route('/create'),
'view' => ViewArticle::route('/{record}'),
'edit' => EditArticle::route('/{record}/edit'),
];
}

/**
* @return Builder<Article>
*/
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()
->withoutGlobalScopes([
SoftDeletingScope::class,
]);
}

public static function shouldSkipAuthorization(): bool
{
return app()->isLocal();
}
}
13 changes: 13 additions & 0 deletions app/Filament/Resources/ArticleResource/Pages/CreateArticle.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace App\Filament\Resources\ArticleResource\Pages;

use App\Filament\Resources\ArticleResource;
use Filament\Resources\Pages\CreateRecord;

class CreateArticle extends CreateRecord
{
protected static string $resource = ArticleResource::class;
}
25 changes: 25 additions & 0 deletions app/Filament/Resources/ArticleResource/Pages/EditArticle.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace App\Filament\Resources\ArticleResource\Pages;

use App\Filament\Resources\ArticleResource;
use Filament\Actions\DeleteAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\RestoreAction;
use Filament\Resources\Pages\EditRecord;

class EditArticle extends EditRecord
{
protected static string $resource = ArticleResource::class;

protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
ForceDeleteAction::make(),
RestoreAction::make(),
];
}
}
21 changes: 21 additions & 0 deletions app/Filament/Resources/ArticleResource/Pages/ListArticles.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace App\Filament\Resources\ArticleResource\Pages;

use App\Filament\Resources\ArticleResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;

class ListArticles extends ListRecords
{
protected static string $resource = ArticleResource::class;

protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}
27 changes: 27 additions & 0 deletions app/Filament/Resources/ArticleResource/Pages/ViewArticle.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace App\Filament\Resources\ArticleResource\Pages;

use App\Filament\Resources\ArticleResource;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\RestoreAction;
use Filament\Resources\Pages\ViewRecord;

class ViewArticle extends ViewRecord
{
protected static string $resource = ArticleResource::class;

protected function getHeaderActions(): array
{
return [
EditAction::make(),
DeleteAction::make(),
ForceDeleteAction::make(),
RestoreAction::make(),
];
}
}
6 changes: 6 additions & 0 deletions app/Models/Article.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace App\Models;

use App\Enums\ArticleCategoryEnum;
use App\Models\Traits\BelongsToUser;
use CyrildeWit\EloquentViewable\Contracts\Viewable;
use CyrildeWit\EloquentViewable\InteractsWithViews;
Expand All @@ -20,6 +21,11 @@ class Article extends Model implements HasMedia, Viewable

public $guarded = ['id'];

protected $casts = [
'category' => ArticleCategoryEnum::class,
'published_at' => 'timestamp',
];

/**
* @return BelongsToMany<Collection>
*/
Expand Down
11 changes: 11 additions & 0 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,15 @@ public function canAccessPanel(Panel $panel): bool
return false;
}
}

/**
* @param Builder<User> $query
* @return Builder<User>
*/
public function scopeManagers(Builder $query): Builder
{
return $query->whereHas('roles', function ($query) {
$query->whereIn('name', [Role::Admin->value, Role::Superadmin->value, Role::Editor->value]);
});
}
}
57 changes: 57 additions & 0 deletions app/Policies/ArticlePolicy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace App\Policies;

use App\Models\Article;
use App\Models\User;

final class ArticlePolicy
{
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('article:viewAny', 'admin');
}

public function view(User $user, Article $article): bool
{
// If users can view any, can view single article
return $this->viewAny($user);
}

public function create(User $user): bool
{
return $user->hasPermissionTo('article:create', 'admin');
}

public function update(User $user, Article $article): bool
{
if ($user->hasPermissionTo('article:updateAny', 'admin')) {
return true;
}

// If users can create, they can update their own
return $this->create($user) && ($user->is($article->user));
}

public function delete(User $user, Article $article): bool
{
if ($user->hasPermissionTo('article:deleteAny', 'admin')) {
return true;
}

// If users can create, they can delete their own
return $this->create($user) && ($user->is($article->user));
}

public function restore(User $user, Article $article): bool
{
return $user->hasPermissionTo('article:restore', 'admin');
}

public function forceDelete(User $user, Article $article): bool
{
return $user->hasPermissionTo('article:forceDelete', 'admin');
}
}
14 changes: 14 additions & 0 deletions config/permission.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,31 @@
'user:view' => 'View User',
'user:assignRole' => 'Assign User Role',
'user:assignPermissions' => 'Assign User Permissions',
'article:create' => 'Create Article',
'article:viewAny' => 'View Article',
'article:updateAny' => 'Update any Article',
'article:deleteAny' => 'Delete any Article',
'article:restore' => 'Restore Deleted Article',
'article:forceDelete' => 'Force Delete Article',
'admin:access' => 'Allow access to Admin panel',
],

'roles' => [
Role::Superadmin->value => [
'user:viewAny', 'user:view', 'user:restore', 'user:assignRole', 'user:assignPermissions',
'article:viewAny', 'article:create', 'article:updateAny', 'article:deleteAny', 'article:restore', 'article:forceDelete',
'admin:access',
],

Role::Admin->value => [
'user:viewAny', 'user:view', 'user:assignRole',
'article:viewAny', 'article:create', 'article:updateAny', 'article:deleteAny', 'article:restore', 'article:forceDelete',
'admin:access',
],

Role::Editor->value => [
// For the moment `article:create` also allows to update and delete own articles
'article:viewAny', 'article:create',
'admin:access',
],
],
Expand Down
3 changes: 2 additions & 1 deletion database/factories/ArticleFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Database\Factories;

use App\Enums\ArticleCategoryEnum;
use App\Models\Article;
use App\Models\User;
use Database\Factories\Traits\RandomTimestamps;
Expand All @@ -25,7 +26,7 @@ public function definition(): array
{
return [
'title' => fake()->name(),
'category' => fake()->name(),
'category' => fake()->randomElement([ArticleCategoryEnum::News->value]),
'published_at' => fake()->date(),
'meta_description' => fake()->text(),
'content' => fake()->text(),
Expand Down
4 changes: 2 additions & 2 deletions database/seeders/ArticleSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ public function run(): void
$imageUrl = fake()->imageUrl(640, 480, null, false);
$article->addMediaFromUrl($imageUrl)->toMediaCollection();

$collections = Collection::factory(2)->createMany([
'network' => $network->id,
$collections = Collection::factory()->count(2)->create([
'network_id' => $network->id,
]);

$article->collections()->attach($collections, ['order_index' => 1]);
Expand Down
Loading

0 comments on commit ecd3f75

Please sign in to comment.