Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: articles crud #134

Merged
merged 13 commits into from
Sep 26, 2023
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();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

notice that You need to remove this to test the permissions

}
}
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
Loading