diff --git a/README.md b/README.md index f1d0c302..a7569417 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ From the project directory, run `./vendor/bin/pest` ## Interacting with the package During development, you may like to actually interact with the FilaCMS UI. In your console, run -```./vendor/bin/workbench serve``` +```./vendor/bin/testbench serve``` You can now load the application at http://localhost:8000/admin @@ -53,3 +53,45 @@ Password: password ## Protecting resources Add the `IsProtectedResource` trait to your Filament resources to have them automatically obey `view ` and `manage ` permissions. + +## Extending the Abstract Content + +To add additional models or tables that extends the AbstractContent, you start by executing `php artisan make:filament-resource {Resource}`. + +This command will generate a Resource file in your App\Filament\Resources folder. Add the next line in your class: +`use Portable\FilaCms\Filament\Resources\AbstractContentResource;` + +Then go to your generated Resource file (e.g. `RecipeResource.php`), and change the `extends Resource` part to `extends AbstractContentResource`. + +You should declare the proper model in your `$model` variable. + +Then go to your model and add the following line: +`use Portable\FilaCms\Models\AbstractContentModel;` +Then change the `extends Model` to `extends AbstractContentModel` + +Next is to create a Plugin class in your `app/Plugins` folder (or create the folder if it's not present yet). The content should look like this (change the appropriate values such as the Resource and the ID): + +~~~ +namespace App\Plugins; + +use App\Filament\Resources\RecipeResource; +use Filament\Panel; +use Filament\Contracts\Plugin; + +class RecipesPlugin implements Plugin +{ + public function getId(): string + { + return 'filacms-recipes'; + } + + public function register(Panel $panel): void + { + $panel->resources([ + RecipeResource::class + ]); + } +} +~~~ + +Finally, add the plugin in your `app/config/fila-cms.php` file \ No newline at end of file diff --git a/composer.json b/composer.json index c2a1e7d9..ac5dc42e 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,7 @@ "mistralys/text-diff": "^2.0", "spatie/laravel-permission": "^6.3", "laravel/framework": "^10.0", + "rawilk/filament-password-input": "^2.0", "ralphjsmit/laravel-filament-seo": "^1.3" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 98a72573..d3bdde94 100644 --- a/composer.lock +++ b/composer.lock @@ -5245,6 +5245,83 @@ ], "time": "2023-11-08T05:53:05+00:00" }, + { + "name": "rawilk/filament-password-input", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/rawilk/filament-password-input.git", + "reference": "bda2631925027c4d6c0067fd71d3b3b6ea8ecc30" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rawilk/filament-password-input/zipball/bda2631925027c4d6c0067fd71d3b3b6ea8ecc30", + "reference": "bda2631925027c4d6c0067fd71d3b3b6ea8ecc30", + "shasum": "" + }, + "require": { + "filament/forms": "^3.2", + "illuminate/contracts": "^10.0|^11.0", + "php": "^8.1|^8.2|^8.3", + "spatie/laravel-package-tools": "^1.14" + }, + "require-dev": { + "larastan/larastan": "^2.6", + "laravel/pint": "^1.0", + "nunomaduro/collision": "^7.9|^8.0", + "orchestra/testbench": "^8.8|^9.0", + "pestphp/pest": "^2.20", + "pestphp/pest-plugin-laravel": "^2.2", + "pestphp/pest-plugin-livewire": "^2.1", + "spatie/laravel-ray": "^1.31" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Rawilk\\FilamentPasswordInput\\FilamentPasswordInputServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Rawilk\\FilamentPasswordInput\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Randall Wilk", + "email": "randall@randallwilk.dev", + "role": "Developer" + } + ], + "description": "Enhanced password input component for filament.", + "homepage": "https://github.com/rawilk/filament-password-input", + "keywords": [ + "Forms", + "filament", + "filament-password-input", + "laravel", + "password", + "rawilk", + "ui" + ], + "support": { + "issues": "https://github.com/rawilk/filament-password-input/issues", + "source": "https://github.com/rawilk/filament-password-input/tree/v2.0.1" + }, + "funding": [ + { + "url": "https://github.com/rawilk", + "type": "github" + } + ], + "time": "2024-03-05T15:00:16+00:00" + }, { "name": "ryangjchandler/blade-capture-directive", "version": "v1.0.0", @@ -13079,5 +13156,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.6.0" } diff --git a/src/Filament/Resources/AbstractContentResource.php b/src/Filament/Resources/AbstractContentResource.php index 68800d4e..bcdc65e9 100644 --- a/src/Filament/Resources/AbstractContentResource.php +++ b/src/Filament/Resources/AbstractContentResource.php @@ -2,6 +2,7 @@ namespace Portable\FilaCms\Filament\Resources; +use Filament\Forms\Form; use Filament\Forms\Components\CheckboxList; use Filament\Forms\Components\DatePicker; use Filament\Forms\Components\Fieldset; @@ -13,16 +14,16 @@ use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; use Filament\Forms\Components\View; -use Filament\Forms\Form; use Filament\Tables; -use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; +use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Filters\TernaryFilter; + use FilamentTiptapEditor\TiptapEditor; use FilamentTiptapEditor\Enums\TiptapOutput; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletingScope; -use Illuminate\Support\Str; use Portable\FilaCms\Filament\Forms\Components\StatusBadge; use Portable\FilaCms\Filament\Resources\AbstractContentResource\Pages; use Portable\FilaCms\Filament\Resources\AbstractContentResource\RelationManagers; @@ -32,6 +33,7 @@ use Portable\FilaCms\Models\Scopes\PublishedScope; use Portable\FilaCms\Models\TaxonomyResource; use RalphJSmit\Filament\SEO\SEO; +use Str; class AbstractContentResource extends AbstractResource { @@ -65,7 +67,7 @@ public static function form(Form $form): Form ->columnSpanFull() ->required(), static::tiptapEditor()->output(\FilamentTiptapEditor\Enums\TiptapOutput::Json), - SEO::make(['description']), + SEO::make(['description']), ]), Tabs\Tab::make('Taxonomies') ->schema([ @@ -177,6 +179,17 @@ public static function table(Table $table): Table ]) ->filters([ Tables\Filters\TrashedFilter::make(), + TernaryFilter::make('is_draft') + ->label('Draft') + ->attribute('is_draft') + ->nullable() + ->placeholder('All Records') + ->falseLabel('Non-Drafts Only') + ->trueLabel('Drafts Only') + ->queries( + true: fn (Builder $query) => $query->where('is_draft', true), + false: fn (Builder $query) => $query->where('is_draft', false), + ) ]) ->actions([ Tables\Actions\DeleteAction::make(), diff --git a/src/Filament/Resources/PageResource/Pages/ListPages.php b/src/Filament/Resources/PageResource/Pages/ListPages.php index 887869c6..5070fc9c 100644 --- a/src/Filament/Resources/PageResource/Pages/ListPages.php +++ b/src/Filament/Resources/PageResource/Pages/ListPages.php @@ -4,8 +4,28 @@ use Portable\FilaCms\Filament\Resources\AbstractContentResource\Pages\ListAbstractContentResources; use Portable\FilaCms\Filament\Resources\PageResource; +use Illuminate\Database\Eloquent\Builder; class ListPages extends ListAbstractContentResources { protected static string $resource = PageResource::class; + + public function isTableSearchable(): bool + { + return true; + } + + protected function applySearchToTableQuery(Builder $query): Builder + { + if (filled($searchQuery = $this->getTableSearch())) { + $searchQuery = '%' . $searchQuery . '%'; + $query->where('title', 'LIKE', $searchQuery) + ->orWhere('slug', 'LIKE', $searchQuery) + ->orWhere('contents', 'LIKE', $searchQuery) + ->orWhere('slug', 'LIKE', $searchQuery) + ->orWhere('slug', 'LIKE', $searchQuery); + } + + return $query; + } } diff --git a/src/Filament/Resources/UserResource.php b/src/Filament/Resources/UserResource.php index 00bd18ca..b0d8db79 100644 --- a/src/Filament/Resources/UserResource.php +++ b/src/Filament/Resources/UserResource.php @@ -8,6 +8,7 @@ use Filament\Tables\Table; use Portable\FilaCms\Filament\Resources\UserResource\Pages; use Portable\FilaCms\Filament\Traits\IsProtectedResource; +use Rawilk\FilamentPasswordInput\Password; class UserResource extends AbstractConfigurableResource { @@ -31,9 +32,11 @@ public static function form(Form $form): Form ->email() ->prefixIcon('heroicon-m-envelope') ->required(), - Forms\Components\TextInput::make('password') - ->password() - ->revealable(), + Password::make('password') + ->regeneratePassword(color: 'warning') + ->copyable(color: 'info') + ->newPasswordLength(16) + ->required(), Forms\Components\Select::make('roles') ->relationship('roles', 'name') ->multiple() diff --git a/src/Providers/FilaCmsServiceProvider.php b/src/Providers/FilaCmsServiceProvider.php index f46ec865..da3b2034 100644 --- a/src/Providers/FilaCmsServiceProvider.php +++ b/src/Providers/FilaCmsServiceProvider.php @@ -28,12 +28,16 @@ public function boot() } if (config('fila-cms.publish_content_routes')) { - $this->loadRoutesFrom(__DIR__.'/../../routes/frontend-routes.php'); + $this->loadRoutesFrom(__DIR__ . '/../../routes/frontend-routes.php'); } //$this->loadRoutesFrom(__DIR__.'/../Routes/web.php'); - $this->loadViewsFrom(__DIR__.'/../../views', 'fila-cms'); - $this->loadMigrationsFrom(__DIR__.'/../../database/migrations'); + $this->loadViewsFrom(__DIR__ . '/../../views', 'fila-cms'); + $this->loadMigrationsFrom(__DIR__ . '/../../database/migrations'); + + \Filament\Support\Facades\FilamentIcon::register([ + 'filament-password-input::regenerate' => 'heroicon-m-key', + ]); Livewire::component('portable.fila-cms.livewire.content-resource-list', \Portable\FilaCms\Livewire\ContentResourceList::class); Livewire::component('portable.fila-cms.livewire.content-resource-show', \Portable\FilaCms\Livewire\ContentResourceShow::class); @@ -51,12 +55,12 @@ public function register() }); $this->publishes([ - __DIR__.'/../../config/fila-cms.php' => config_path('fila-cms.php'), + __DIR__ . '/../../config/fila-cms.php' => config_path('fila-cms.php'), ], 'fila-cms-config'); // use the vendor configuration file as fallback $this->mergeConfigFrom( - __DIR__.'/../../config/fila-cms.php', + __DIR__ . '/../../config/fila-cms.php', 'fila-cms' ); @@ -72,7 +76,6 @@ public function register() } else { return app('Illuminate\Foundation\Vite')('vendor/portable/filacms/resources/css/filacms.css'); } - } catch (\Exception $e) { return ''; } diff --git a/tests/Filament/PageResourceTest.php b/tests/Filament/PageResourceTest.php index 78b9aa73..1b1b0d63 100644 --- a/tests/Filament/PageResourceTest.php +++ b/tests/Filament/PageResourceTest.php @@ -13,6 +13,7 @@ use Portable\FilaCms\Models\TaxonomyTerm; use Portable\FilaCms\Tests\TestCase; use Spatie\Permission\Models\Role; +use RalphJSmit\Laravel\SEO\Models\SEO; class PageResourceTest extends TestCase { @@ -77,6 +78,26 @@ public function test_can_create_record(): void ]); } + public function test_can_save_seo(): void + { + $data = $this->generateModel(true); + $data['seo.description'] = 'Test Description'; + $data['is_draft'] = 0; + $data['publish_at'] = now()->subday(); + $data['expire_at'] = now()->addDay(); + + Livewire::test(TargetResource\Pages\CreatePage::class) + ->fillForm($data) + ->call('create') + ->assertHasNoFormErrors(); + + // check last record + $model = TargetModel::orderBy('id', 'desc')->first(); + + $this->assertTrue($model->Seo instanceof SEO); + + } + public function test_can_render_edit_page(): void { $data = $this->generateModel(); diff --git a/tests/Listeners/ServeCommandStartedListener.php b/tests/Listeners/ServeCommandStartedListener.php index ffe30212..ea97c555 100644 --- a/tests/Listeners/ServeCommandStartedListener.php +++ b/tests/Listeners/ServeCommandStartedListener.php @@ -29,9 +29,10 @@ public function handle(): void Artisan::call('fila-cms:install', ['--publish-config' => true,'--run-migrations' => true,'--add-user-traits' => true]); // Ensure there's an admin user - $admin = User::where('email', 'admin@test.com')->first(); + $userModel = config('auth.providers.users.model'); + $admin = $userModel::where('email', 'admin@test.com')->first(); if(!$admin) { - $admin = new User(); + $admin = new $userModel(); } $admin->email = 'admin@test.com'; $admin->password = Hash::make('password');