diff --git a/app/Filament/Admin/Actions/MergeBulkAction.php b/app/Filament/Admin/Actions/MergeBulkAction.php new file mode 100644 index 0000000..87a179a --- /dev/null +++ b/app/Filament/Admin/Actions/MergeBulkAction.php @@ -0,0 +1,70 @@ +label(__('actions.merge.multiple.label')); + + $this->modalHeading(fn () => __('actions.merge.multiple.modal.heading', ['label' => $this->getPluralModelLabel()])); + + $this->modalSubmitActionLabel(__('actions.merge.multiple.modal.actions.merge.label')); + + $this->successNotificationTitle(__('actions.merge.multiple.notifications.merged.title')); + + $this->color('warning'); + + $this->outlined(); + + // $this->icon(FilamentIcon::resolve('actions::merge-action') ?? 'heroicon-m-trash'); + + $this->requiresConfirmation(); + + $this->modalIcon(FilamentIcon::resolve('actions::merge-action.modal') ?? 'heroicon-o-trash'); + + $this->action(function () { + $this->process(static function (Collection $records) { + $keep = $records + ->sortBy('id') + ->first(); + + $records = $records->reject(fn (Model $record) => $record->is($keep)); + + $data = $keep->toArray(); + + $records->each(function (Model $record) use (&$data) { + $data = array_merge_recursive($data, $record->toArray()); + }); + + $keep->update($data); + + // TODO: handle relationships + + $records->each->delete(); + }); + + $this->success(); + }); + + $this->deselectRecordsAfterCompletion(); + } +} diff --git a/app/Filament/Admin/Resources/LocationResource.php b/app/Filament/Admin/Resources/LocationResource.php new file mode 100644 index 0000000..74de933 --- /dev/null +++ b/app/Filament/Admin/Resources/LocationResource.php @@ -0,0 +1,96 @@ +schema([ + TextInput::make('name') + ->label(__('app.field.name')) + ->columnSpanFull() + ->maxLength(200) + ->required(), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + TextColumn::make('id') + ->label(__('app.field.id')) + ->sortable() + ->shrink(), + + TextColumn::make('name') + ->label(__('app.field.name')) + ->searchable() + ->sortable(), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\ActionGroup::make([ + Tables\Actions\EditAction::make(), + Tables\Actions\DeleteAction::make(), + ]), + ]) + ->bulkActions([ + MergeBulkAction::make(), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ManageLocations::route('/'), + ]; + } +} diff --git a/app/Filament/Admin/Resources/LocationResource/Pages/ManageLocations.php b/app/Filament/Admin/Resources/LocationResource/Pages/ManageLocations.php new file mode 100644 index 0000000..6d5bb56 --- /dev/null +++ b/app/Filament/Admin/Resources/LocationResource/Pages/ManageLocations.php @@ -0,0 +1,24 @@ + */ + use HasFactory; + use HasTranslations; + + protected static string $factory = LocationFactory::class; + + protected $fillable = [ + 'name', + ]; + + public array $translatable = [ + 'name', + ]; +} diff --git a/app/Policies/LocationPolicy.php b/app/Policies/LocationPolicy.php new file mode 100644 index 0000000..debe574 --- /dev/null +++ b/app/Policies/LocationPolicy.php @@ -0,0 +1,67 @@ +delete($user, $location); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, Location $location): bool + { + return $this->delete($user, $location); + } +} diff --git a/database/factories/LocationFactory.php b/database/factories/LocationFactory.php new file mode 100644 index 0000000..8bac71c --- /dev/null +++ b/database/factories/LocationFactory.php @@ -0,0 +1,25 @@ + + */ +class LocationFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->city(), + ]; + } +} diff --git a/database/migrations/0001_01_02_000002_create_locations_table.php b/database/migrations/0001_01_02_000002_create_locations_table.php new file mode 100644 index 0000000..e5f9496 --- /dev/null +++ b/database/migrations/0001_01_02_000002_create_locations_table.php @@ -0,0 +1,19 @@ +id(); + $table->json('name'); + $table->timestamps(); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index f5e87fc..0480436 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -5,6 +5,8 @@ namespace Database\Seeders; // use Illuminate\Database\Console\Seeds\WithoutModelEvents; + +use App\Models\Location; use App\Models\User; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\Mail; @@ -21,5 +23,9 @@ public function run(): void User::factory(['email' => 'admin@example.com']) ->superAdmin() ->create(); + + Location::factory() + ->count(50) + ->create(); } } diff --git a/lang/en/actions.php b/lang/en/actions.php new file mode 100644 index 0000000..2cf6c7f --- /dev/null +++ b/lang/en/actions.php @@ -0,0 +1,45 @@ + [ + 'single' => [ + 'label' => 'Merge', + 'modal' => [ + 'heading' => 'Merge :label', + 'actions' => [ + 'merge' => [ + 'label' => 'Merge', + ], + ], + ], + + 'notifications' => [ + 'merged' => [ + 'title' => 'Merged', + ], + ], + ], + + 'multiple' => [ + 'label' => 'Merge selected', + 'modal' => [ + 'heading' => 'Merge selected :label', + 'actions' => [ + 'merge' => [ + 'label' => 'Merge', + ], + ], + ], + + 'notifications' => [ + 'merged' => [ + 'title' => 'Merged', + ], + ], + ], + ], + +]; diff --git a/lang/en/app.php b/lang/en/app.php index 62e4168..212c421 100644 --- a/lang/en/app.php +++ b/lang/en/app.php @@ -21,4 +21,10 @@ ], ], + 'location' => [ + 'label' => [ + 'singular' => 'location', + 'plural' => 'locations', + ], + ], ]; diff --git a/tests/Feature/Admin/LocationsTest.php b/tests/Feature/Admin/LocationsTest.php new file mode 100644 index 0000000..e26eb00 --- /dev/null +++ b/tests/Feature/Admin/LocationsTest.php @@ -0,0 +1,93 @@ +actingAs( + User::factory() + ->superAdmin() + ->create() + ); + } + + #[Test] + public function superadmins_can_list_locations(): void + { + $locations = Location::factory() + ->count(10) + ->create(); + + Livewire::test(ManageLocations::class) + ->assertSuccessful() + ->assertTableColumnExists('id') + ->assertTableColumnExists('name') + ->assertCountTableRecords($locations->count()); + // ->sortTable('id', 'asc') + // ->assertCanSeeTableRecords($locations->sortBy('id'), inOrder: true) + // ->sortTable('name', 'asc') + // ->assertCanSeeTableRecords($locations->sortBy('name'), inOrder: true); + } + + #[Test] + public function superadmins_can_create_a_location(): void + { + Livewire::test(ManageLocations::class) + ->callAction(CreateAction::class, [ + 'name' => fake()->word(), + ]) + ->assertHasNoErrors() + ->assertCountTableRecords(1); + } + + #[Test] + public function superadmins_can_update_a_location(): void + { + $location = Location::factory() + ->create(); + + Livewire::test(ManageLocations::class) + ->callTableAction(EditAction::class, $location->id, [ + 'name' => 'Updated Location', + ]) + ->assertHasNoTableActionErrors(); + + $this->assertEquals('Updated Location', $location->refresh()->name); + } + + #[Test] + public function superadmins_can_bulk_merge_two_locations(): void + { + $locations = Location::factory() + ->count(2) + ->create(); + + Livewire::test(ManageLocations::class) + ->assertCountTableRecords(2) + ->callTableBulkAction(MergeBulkAction::class, $locations->pluck('id')) + ->assertHasNoTableActionErrors() + ->assertCountTableRecords(1) + ->assertSeeText($locations->pluck('name')->join(', ')); + } +}