Merge pull request 'feat: add TextWidget' (#9) from feat/add-text-widgets into dev
Reviewed-on: https://codeberg.org/headshed/share-lt/pulls/9
This commit is contained in:
commit
c83028b4d4
18 changed files with 536 additions and 1 deletions
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\TextWidgets\Pages;
|
||||
|
||||
use App\Filament\Resources\TextWidgets\TextWidgetResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateTextWidget extends CreateRecord
|
||||
{
|
||||
protected static string $resource = TextWidgetResource::class;
|
||||
}
|
||||
19
app/Filament/Resources/TextWidgets/Pages/EditTextWidget.php
Normal file
19
app/Filament/Resources/TextWidgets/Pages/EditTextWidget.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\TextWidgets\Pages;
|
||||
|
||||
use App\Filament\Resources\TextWidgets\TextWidgetResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditTextWidget extends EditRecord
|
||||
{
|
||||
protected static string $resource = TextWidgetResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
19
app/Filament/Resources/TextWidgets/Pages/ListTextWidgets.php
Normal file
19
app/Filament/Resources/TextWidgets/Pages/ListTextWidgets.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\TextWidgets\Pages;
|
||||
|
||||
use App\Filament\Resources\TextWidgets\TextWidgetResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListTextWidgets extends ListRecords
|
||||
{
|
||||
protected static string $resource = TextWidgetResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\TextWidgets\Schemas;
|
||||
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class TextWidgetForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('title')
|
||||
->required()
|
||||
->live(onBlur: true),
|
||||
TextInput::make('description')
|
||||
->nullable(),
|
||||
Textarea::make('content')
|
||||
->rows(5)
|
||||
->columnSpanFull(),
|
||||
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\TextWidgets\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class TextWidgetsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('title')
|
||||
->label('Title')
|
||||
->sortable()
|
||||
->searchable(),
|
||||
TextColumn::make('created_at')
|
||||
->label('Created')
|
||||
->dateTime('M d, Y H:i')
|
||||
->sortable(),
|
||||
TextColumn::make('updated_at')
|
||||
->label('Updated')
|
||||
->dateTime('M d, Y H:i')
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
50
app/Filament/Resources/TextWidgets/TextWidgetResource.php
Normal file
50
app/Filament/Resources/TextWidgets/TextWidgetResource.php
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\TextWidgets;
|
||||
|
||||
use App\Filament\Resources\TextWidgets\Pages\CreateTextWidget;
|
||||
use App\Filament\Resources\TextWidgets\Pages\EditTextWidget;
|
||||
use App\Filament\Resources\TextWidgets\Pages\ListTextWidgets;
|
||||
use App\Filament\Resources\TextWidgets\Schemas\TextWidgetForm;
|
||||
use App\Filament\Resources\TextWidgets\Tables\TextWidgetsTable;
|
||||
use App\Models\TextWidget;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class TextWidgetResource extends Resource
|
||||
{
|
||||
protected static ?string $model = TextWidget::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::DocumentText;
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'title';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return TextWidgetForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return TextWidgetsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListTextWidgets::route('/'),
|
||||
'create' => CreateTextWidget::route('/create'),
|
||||
'edit' => EditTextWidget::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
72
app/Http/Controllers/TextWidgetController.php
Normal file
72
app/Http/Controllers/TextWidgetController.php
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\TextWidget;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class TextWidgetController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return TextWidget::all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if (! $user || $user->email !== config('app.admin_email')) {
|
||||
return response()->json(['message' => 'Forbidden'], 403);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'content' => 'required|string',
|
||||
]);
|
||||
|
||||
return TextWidget::create($validated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(TextWidget $textWidget)
|
||||
{
|
||||
return $textWidget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, TextWidget $textWidget)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => 'sometimes|required|string|max:255',
|
||||
'description' => 'sometimes|nullable|string',
|
||||
'content' => 'sometimes|required|string',
|
||||
]);
|
||||
|
||||
$textWidget->update($validated);
|
||||
|
||||
return $textWidget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(TextWidget $textWidget)
|
||||
{
|
||||
$textWidget->delete();
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
}
|
||||
17
app/Models/TextWidget.php
Normal file
17
app/Models/TextWidget.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TextWidget extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'description',
|
||||
'content',
|
||||
];
|
||||
}
|
||||
15
cmd/curl_get_text_widget.sh
Executable file
15
cmd/curl_get_text_widget.sh
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# TOKEN="your_api_token_here"
|
||||
# ensure to have set TOKEN to a valid value before running
|
||||
# ideally add this to an .envrc file and source it
|
||||
# tokens need to be created with tinker or similar method
|
||||
|
||||
|
||||
URL='http://127.0.0.1:8000/api/text-widgets'
|
||||
|
||||
curl -s -X GET \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
$URL
|
||||
|
||||
13
cmd/curl_get_text_widgets_anon.sh
Executable file
13
cmd/curl_get_text_widgets_anon.sh
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# this should fail, no token provided
|
||||
# users need to be authenticated and have been
|
||||
# granted access to view text widgets by being given
|
||||
# a token
|
||||
|
||||
URL='http://127.0.0.1:8000/api/text-widgets'
|
||||
|
||||
curl -s -X GET \
|
||||
-H "Accept: application/json" \
|
||||
$URL
|
||||
|
||||
20
cmd/curl_post_text_widget.sh
Executable file
20
cmd/curl_post_text_widget.sh
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# TOKEN="your_api_token_here"
|
||||
# ensure to have set TOKEN to a valid value before running
|
||||
# ideally add this to an .envrc file and source it
|
||||
# only the admin user can create entries so this should
|
||||
# fail unless .env has ADMIN_EMAIL set to the user that
|
||||
# the token belongs to
|
||||
|
||||
URL='http://127.0.0.1:8000/api/text-widgets'
|
||||
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"title": "Yet Another New text widget Title",
|
||||
"content": "This is the content yet again of the new text widget."
|
||||
}' \
|
||||
$URL
|
||||
16
cmd/curl_post_text_widget_anon.sh
Executable file
16
cmd/curl_post_text_widget_anon.sh
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# this should fail as no token is provided
|
||||
# user is not authenticated
|
||||
# no token has been granted
|
||||
|
||||
URL='http://127.0.0.1:8000/api/text-widgets'
|
||||
|
||||
curl -X POST \
|
||||
-H "Accept: application/json" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"title": "Yet Another New Entry Title",
|
||||
"content": "This is the content yet again of the new entry."
|
||||
}' \
|
||||
$URL
|
||||
25
database/factories/TextWidgetFactory.php
Normal file
25
database/factories/TextWidgetFactory.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\TextWidget>
|
||||
*/
|
||||
class TextWidgetFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'title' => $this->faker->sentence(),
|
||||
'description' => $this->faker->paragraph(),
|
||||
'content' => $this->faker->text(500),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?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('text_widgets', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('title');
|
||||
$table->text('description')->nullable();
|
||||
$table->mediumText('content')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('text_widgets');
|
||||
}
|
||||
};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\EntryController;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\TextWidgetController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware('auth:sanctum')->group(function () {
|
||||
|
|
@ -10,4 +10,10 @@ Route::middleware('auth:sanctum')->group(function () {
|
|||
Route::get('/entries/{entry}', [EntryController::class, 'show']);
|
||||
Route::put('/entries/{entry}', [EntryController::class, 'update']);
|
||||
Route::delete('/entries/{entry}', [EntryController::class, 'destroy']);
|
||||
|
||||
Route::get('/text-widgets', [TextWidgetController::class, 'index']);
|
||||
Route::post('/text-widgets', [TextWidgetController::class, 'store']);
|
||||
Route::get('/text-widgets/{textWidget}', [TextWidgetController::class, 'show']);
|
||||
Route::put('/text-widgets/{textWidget}', [TextWidgetController::class, 'update']);
|
||||
Route::delete('/text-widgets/{textWidget}', [TextWidgetController::class, 'destroy']);
|
||||
});
|
||||
|
|
|
|||
92
tests/Unit/TextWidgetApiTest.php
Normal file
92
tests/Unit/TextWidgetApiTest.php
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
use App\Models\TextWidget;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
test('can list text widgets', function () {
|
||||
$user = User::factory()->create();
|
||||
TextWidget::factory()->count(3)->create();
|
||||
|
||||
$response = $this->actingAs($user)->getJson('/api/text-widgets');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonCount(3);
|
||||
});
|
||||
|
||||
test('can create a text widget', function () {
|
||||
$admin = User::factory()->create(['email' => config('app.admin_email')]);
|
||||
|
||||
$this->actingAs($admin);
|
||||
|
||||
$data = [
|
||||
'title' => 'Sample Title',
|
||||
'content' => 'Sample Content',
|
||||
];
|
||||
|
||||
$response = $this->postJson('/api/text-widgets', $data);
|
||||
|
||||
$response->assertCreated()
|
||||
->assertJsonFragment($data);
|
||||
});
|
||||
|
||||
test('can show a text widget', function () {
|
||||
$user = User::factory()->create();
|
||||
$textWidget = TextWidget::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->getJson("/api/text-widgets/{$textWidget->id}");
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonFragment(['id' => $textWidget->id]);
|
||||
});
|
||||
|
||||
test('can update a text widget', function () {
|
||||
$user = User::factory()->create();
|
||||
$textWidget = TextWidget::factory()->create();
|
||||
$data = ['title' => 'Updated Title'];
|
||||
|
||||
$response = $this->actingAs($user)->putJson("/api/text-widgets/{$textWidget->id}", $data);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonFragment($data);
|
||||
});
|
||||
|
||||
test('can delete a text widget', function () {
|
||||
$user = User::factory()->create();
|
||||
$textWidget = TextWidget::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->deleteJson("/api/text-widgets/{$textWidget->id}");
|
||||
|
||||
$response->assertNoContent();
|
||||
|
||||
$this->assertDatabaseMissing('text_widgets', ['id' => $textWidget->id]);
|
||||
});
|
||||
|
||||
test('only admin can create text widgets', function () {
|
||||
$adminEmail = Config::get('app.admin_email');
|
||||
$user = User::factory()->create(['email' => $adminEmail]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->postJson('/api/text-widgets', ['title' => 'Test', 'content' => 'Test content'])
|
||||
->assertCreated();
|
||||
|
||||
$nonAdmin = User::factory()->create(['email' => 'nonadmin@example.com']);
|
||||
|
||||
$this->actingAs($nonAdmin)
|
||||
->postJson('/api/text-widgets', ['title' => 'Test', 'content' => 'Test content'])
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('authenticated users can read text widgets', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->getJson('/api/text-widgets')
|
||||
->assertOk();
|
||||
|
||||
$this->getJson('/api/text-widgets')
|
||||
->assertOk();
|
||||
});
|
||||
15
tests/Unit/TextWidgetModelTest.php
Normal file
15
tests/Unit/TextWidgetModelTest.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
use App\Models\TextWidget;
|
||||
|
||||
it('has correct fillable attributes', function () {
|
||||
$expected = [
|
||||
'title',
|
||||
'description',
|
||||
'content',
|
||||
];
|
||||
|
||||
$entry = new TextWidget;
|
||||
|
||||
expect($entry->getFillable())->toEqual($expected);
|
||||
});
|
||||
47
tests/Unit/TextWidgetResourceTest.php
Normal file
47
tests/Unit/TextWidgetResourceTest.php
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
use App\Filament\Resources\Entries\EntryResource;
|
||||
use App\Filament\Resources\TextWidgets\TextWidgetResource;
|
||||
use App\Models\TextWidget;
|
||||
use ReflectionClass;
|
||||
|
||||
it('references the correct model and record title attribute', function () {
|
||||
$defaults = (new ReflectionClass(TextWidgetResource::class))->getDefaultProperties();
|
||||
|
||||
expect($defaults['model'] ?? null)->toBe(TextWidget::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',
|
||||
'description' => 'This is a test entry.',
|
||||
'content' => '<p>This is the content of the test entry.</p>',
|
||||
];
|
||||
|
||||
$entry = new TextWidget;
|
||||
$entry->fill($data);
|
||||
|
||||
expect($entry->title)->toBe('Test Entry');
|
||||
});
|
||||
|
||||
it('deletes a record correctly', function () {
|
||||
$entry = TextWidget::create([
|
||||
'title' => 'Test Entry to Delete',
|
||||
'description' => 'This is a test entry.',
|
||||
'content' => '<p>This is the content of the test entry.</p>',
|
||||
]);
|
||||
|
||||
$entryId = $entry->id;
|
||||
$entry->delete();
|
||||
|
||||
$deletedEntry = TextWidget::find($entryId);
|
||||
expect($deletedEntry)->toBeNull();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue