added initial entries model

This commit is contained in:
jon brookes 2026-01-02 13:58:06 +00:00
parent 11f1dc6895
commit a0a1c08ece
19 changed files with 487 additions and 6 deletions

View file

@ -5,3 +5,8 @@
added: laravel 12 added: laravel 12
added: AGPLv3 added: AGPLv3
## 2026-01-02
added initial model and filament resource

View file

@ -219,7 +219,7 @@ If you develop a new program, and you want it to be of the greatest possible use
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
test share-lt
Copyright (C) 2026 jon Copyright (C) 2026 jon
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

View file

@ -0,0 +1,58 @@
<?php
namespace App\Filament\Resources\Entries;
use App\Filament\Resources\Entries\Pages\CreateEntry;
use App\Filament\Resources\Entries\Pages\EditEntry;
use App\Filament\Resources\Entries\Pages\ListEntries;
use App\Filament\Resources\Entries\Pages\ViewEntry;
use App\Filament\Resources\Entries\Schemas\EntryForm;
use App\Filament\Resources\Entries\Schemas\EntryInfolist;
use App\Filament\Resources\Entries\Tables\EntriesTable;
use App\Models\Entry;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
class EntryResource extends Resource
{
protected static ?string $model = Entry::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
protected static ?string $recordTitleAttribute = 'title';
public static function form(Schema $schema): Schema
{
return EntryForm::configure($schema);
}
public static function infolist(Schema $schema): Schema
{
return EntryInfolist::configure($schema);
}
public static function table(Table $table): Table
{
return EntriesTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListEntries::route('/'),
'create' => CreateEntry::route('/create'),
'view' => ViewEntry::route('/{record}'),
'edit' => EditEntry::route('/{record}/edit'),
];
}
}

View file

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\Entries\Pages;
use App\Filament\Resources\Entries\EntryResource;
use Filament\Resources\Pages\CreateRecord;
class CreateEntry extends CreateRecord
{
protected static string $resource = EntryResource::class;
}

View file

@ -0,0 +1,21 @@
<?php
namespace App\Filament\Resources\Entries\Pages;
use App\Filament\Resources\Entries\EntryResource;
use Filament\Actions\DeleteAction;
use Filament\Actions\ViewAction;
use Filament\Resources\Pages\EditRecord;
class EditEntry extends EditRecord
{
protected static string $resource = EntryResource::class;
protected function getHeaderActions(): array
{
return [
ViewAction::make(),
DeleteAction::make(),
];
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Entries\Pages;
use App\Filament\Resources\Entries\EntryResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListEntries extends ListRecords
{
protected static string $resource = EntryResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Entries\Pages;
use App\Filament\Resources\Entries\EntryResource;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;
class ViewEntry extends ViewRecord
{
protected static string $resource = EntryResource::class;
protected function getHeaderActions(): array
{
return [
EditAction::make(),
];
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace App\Filament\Resources\Entries\Schemas;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Schema;
use Illuminate\Support\Str;
class EntryForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('title')
->required()
->reactive()
->afterStateUpdated(function ($state, $set): void {
$set('slug', Str::slug((string) $state));
}),
TextInput::make('slug')
->required()
->disabled(),
Textarea::make('description')
->columnSpanFull(),
Toggle::make('is_published')
->required(),
Toggle::make('is_featured')
->required(),
DatePicker::make('published_at'),
RichEditor::make('content')
->columnSpanFull(),
]);
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace App\Filament\Resources\Entries\Schemas;
use Filament\Infolists\Components\IconEntry;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Schema;
class EntryInfolist
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
TextEntry::make('title'),
TextEntry::make('slug'),
TextEntry::make('description')
->placeholder('-')
->columnSpanFull(),
IconEntry::make('is_published')
->boolean(),
IconEntry::make('is_featured')
->boolean(),
TextEntry::make('published_at')
->date()
->placeholder('-'),
TextEntry::make('content')
->placeholder('-')
->columnSpanFull(),
TextEntry::make('created_at')
->dateTime()
->placeholder('-'),
TextEntry::make('updated_at')
->dateTime()
->placeholder('-'),
]);
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace App\Filament\Resources\Entries\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class EntriesTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('title')
->searchable(),
TextColumn::make('slug')
->searchable(),
IconColumn::make('is_published')
->boolean(),
IconColumn::make('is_featured')
->boolean(),
TextColumn::make('published_at')
->date()
->sortable(),
TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->recordActions([
ViewAction::make(),
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}

18
app/Models/Entry.php Normal file
View file

@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Entry extends Model
{
protected $fillable = [
'title',
'slug',
'description',
'is_published',
'is_featured',
'published_at',
'content',
];
}

View file

@ -29,7 +29,7 @@ class AdminPanelProvider extends PanelProvider
->path('admin') ->path('admin')
->login() ->login()
->colors([ ->colors([
'primary' => Color::Amber, 'primary' => Color::Blue,
]) ])
->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources') ->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\Filament\Pages') ->discoverPages(in: app_path('Filament/Pages'), for: 'App\Filament\Pages')

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('entries', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->boolean('is_published')->default(false);
$table->boolean('is_featured')->default(false);
$table->date('published_at')->nullable();
$table->mediumText('content')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('entries');
}
};

View file

@ -12,4 +12,3 @@ It is a way to create apps that have all the features of an advanced app with ma
There are so many things to do, we don't have the time to re-write or re-invent what is already achieved by filament and likely, better than we could create ourselves or likely dream of. There are so many things to do, we don't have the time to re-write or re-invent what is already achieved by filament and likely, better than we could create ourselves or likely dream of.

View file

@ -0,0 +1,86 @@
# 2026-01-02
## Entry model
```bash
php artisan make:model -m Entry
```
creates skeleton files for a model and migration
we edit the migration to hold the fields we desire to be present in each record
just the minimal are added for now, paying attention to the amount of data that these may need to hold
* string (varchar): ~255 chars default.
* text: ~65 KB.
* mediumText: ~16 MB.
```bash
php artisan migrate
```
runs the migration to create the table
## Entry filament resource
```bash
php artisan filament:resource Entry
The "title attribute" is used to label each record in the UI.
You can leave this blank if records do not have a title.
┌ What is the title attribute for this model? ─────────────────┐
│ title │
└──────────────────────────────────────────────────────────────┘
┌ Would you like to generate a read-only view page for the resource? ┐
│ Yes │
└────────────────────────────────────────────────────────────────────┘
┌ Should the configuration be generated from the current database columns? ┐
│ Yes │
└──────────────────────────────────────────────────────────────────────────┘
INFO Filament resource [App\Filament\Resources\Entries\EntryResource] created successfully.
```
There is more work to do on this model but for now, a new resource is added to the admin panel by which records can be added, edited, listed.
Full CRUD is already possible. This is amazing if you think how long this would have taken to create all this manually.
### slugify
```php
TextInput::make('title')
->required()
->reactive()
->afterStateUpdated(function ($state, $set): void {
$set('slug', Str::slug((string) $state));
}),
TextInput::make('slug')
->required(),
```
adding reactive and afterStateUpdted to title automatically creates a safe slug
I class things like this creature comforts of the framework and it shows how simple it can be to make fields active and updated by previous entries
For the user, this reduces error and confusion over what a url should be
### rich editor
just changing the TextInput for content
```php
RichEditor::make('content')
->columnSpanFull(),
```
to `RichEditr` gives us a 'tiptap' rich text editor
already, this is feeling more like a mini CMS, with relatively little effort

View file

@ -13,7 +13,7 @@
pest()->extend(Tests\TestCase::class) pest()->extend(Tests\TestCase::class)
->use(Illuminate\Foundation\Testing\RefreshDatabase::class) ->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
->in('Feature'); ->in('Feature', 'Unit');
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View file

@ -6,5 +6,15 @@ use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase abstract class TestCase extends BaseTestCase
{ {
// /**
* Creates the application.
*/
public function createApplication(): \Illuminate\Foundation\Application
{
$app = require __DIR__ . '/../bootstrap/app.php';
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
return $app;
}
} }

View file

@ -0,0 +1,19 @@
<?php
use App\Models\Entry;
it('has correct fillable attributes', function () {
$expected = [
'title',
'slug',
'description',
'is_published',
'is_featured',
'published_at',
'content',
];
$entry = new Entry();
expect($entry->getFillable())->toEqual($expected);
});

View file

@ -0,0 +1,53 @@
<?php
use App\Filament\Resources\Entries\EntryResource;
use App\Models\Entry;
it('references the correct model and record title attribute', function () {
$defaults = (new ReflectionClass(EntryResource::class))->getDefaultProperties();
expect($defaults['model'] ?? null)->toBe(Entry::class);
expect($defaults['recordTitleAttribute'] ?? null)->toBe('title');
});
it('defines the expected pages', function () {
$pages = EntryResource::getPages();
expect(array_keys($pages))->toEqual(['index', 'create', 'view', 'edit']);
});
it('accepts new record when entered via the form schema', function () {
$data = [
'title' => 'Test Entry',
'slug' => 'test-entry',
'description' => 'This is a test entry.',
'is_published' => true,
'is_featured' => false,
'published_at' => now()->toDateString(),
'content' => '<p>This is the content of the test entry.</p>',
];
$entry = new Entry();
$entry->fill($data);
expect($entry->slug)->toBe('test-entry');
});
it('deletes a record correctly', function () {
$entry = Entry::create([
'title' => 'Test Entry to Delete',
'slug' => 'test-entry-to-delete',
'description' => 'This is a test entry.',
'is_published' => false,
'is_featured' => false,
'published_at' => null,
'content' => '<p>This is the content of the test entry.</p>',
]);
$entryId = $entry->id;
$entry->delete();
$deletedEntry = Entry::find($entryId);
expect($deletedEntry)->toBeNull();
});