From 28e974f241aed8191ef61d81ce416de91b159b0b Mon Sep 17 00:00:00 2001 From: Rudi van Zandwijk Date: Fri, 9 Aug 2024 19:05:28 +0200 Subject: [PATCH 1/2] add archive and unarchive page actions --- README.md | 70 +++++++++++++++- resources/lang/en/actions.php | 65 +++++++++++++++ resources/lang/nl/actions.php | 65 +++++++++++++++ src/Actions/ArchiveAction.php | 62 ++++++++++++++ src/Actions/UnArchiveAction.php | 70 ++++++++++++++++ tests/FilamentArchivableTest.php | 83 ++++++++++++++++++- .../ModelWithArchivableTraitResource.php | 14 ++++ .../Pages/EditPage.php | 21 +++++ 8 files changed, 443 insertions(+), 7 deletions(-) create mode 100644 resources/lang/en/actions.php create mode 100644 resources/lang/nl/actions.php create mode 100644 src/Actions/ArchiveAction.php create mode 100644 src/Actions/UnArchiveAction.php create mode 100644 tests/TestResources/ModelWithArchivableTraitResource/Pages/EditPage.php diff --git a/README.md b/README.md index 9a2fe51..eb6f66e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Filament plugin for archiving and unarchiving table records (eloquent models) based on the [Laravel Archivable package by Joe Butcher](https://github.com/joelbutcher/laravel-archivable). -This filament plugin adds an [ArchiveAction](#archiveunarchive-actions), an [UnArchiveAction](#archiveunarchive-actions) and a [ArchivedFilter](#filtering) to your resource tables. It's also possible to [add custom row-classes for archived records](#add-custom-classes-to-archived-rows). +This filament plugin adds an [ArchiveAction](#archiveunarchive-actions), an [UnArchiveAction](#archiveunarchive-actions) and a [ArchivedFilter](#filtering) to your resource tables. It's also possible to [add custom row-classes for archived records](#add-custom-classes-to-archived-rows). In addition to table-actions, the package provides also page-actions for view/edit pages to archive and unarchive your records. ## Requirements @@ -48,7 +48,7 @@ Follow his installation instructions, which -in short- instructs: ## Usage -### Archive and Unarchive actions +### Archive and UnArchive table actions As soon as the ```Archivable```-trait from the [Laravel Archivable package](#installation) is added to the model, it is possible to add the following actions to the corresponding resource table: ```php @@ -78,7 +78,7 @@ It will show the ```ArchiveAction``` on records that aren't archived, and will s > You should add **both** actions to the same table. The action itself wil determine if it should be shown on the record. -The actions are normal table actions, similar to the [Delete](https://filamentphp.com/docs/3.x/actions/prebuilt-actions/delete) and [Restore](https://filamentphp.com/docs/3.x/actions/prebuilt-actions/restore) actions of FilamentPHP. You can add all features that are described in the [FilamentPHP Table Actions Documentation](https://filamentphp.com/docs/3.x/tables/actions), like: +The actions are normal **table** actions, similar to the [Delete](https://filamentphp.com/docs/3.x/actions/prebuilt-actions/delete) and [Restore](https://filamentphp.com/docs/3.x/actions/prebuilt-actions/restore) table actions of FilamentPHP. You can add all features that are described in the [FilamentPHP Table Actions Documentation](https://filamentphp.com/docs/3.x/tables/actions), like: - ```hiddenLabel()``` - ```tooltip()``` @@ -92,7 +92,49 @@ ArchiveAction::make() ->tooltip('Archive'), ``` -The actions call the ```$model->archive()``` and ```$model->unArchive()``` methods that are provided by the [Laravel Achivable package](https://github.com/joelbutcher/laravel-archivable?tab=readme-ov-file#extensions). +The table actions call the ```$model->archive()``` and ```$model->unArchive()``` methods that are provided by the [Laravel Achivable package](https://github.com/joelbutcher/laravel-archivable?tab=readme-ov-file#extensions). + +### Archive and UnArchive page actions +As soon as the ```Archivable```-trait from the [Laravel Archivable package](#installation) is added to the model, it is possible to add the following actions to the resource **pages**: + +```php +// in e.g. Filament/PostResource/EditPage.php +use Okeonline\FilamentArchivable\Actions\ArchiveAction; +use Okeonline\FilamentArchivable\Actions\UnArchiveAction; + +// ... +protected function getHeaderActions(): array +{ + return [ + ArchiveAction::make(), + UnArchiveAction::make(), + ]; +} +``` + +It will show the ```ArchiveAction``` on records that aren't archived, and will show the ```UnArchiveAction``` on those which are currently archived: + +-->screenshot<-- + +> Be aware that there is a difference between **table** actions and **normal (page)** actions. You can not use the page actions as a table action, vice versa. Nevertheless, the business-logic is the same. *Read [this page](https://filamentphp.com/docs/3.x/actions/overview) for more information about the difference.* + +> You should add **both** actions to the same page. The action itself wil determine if it should be shown on the record. Check -->internal<--- if you want to edit (and unarchive) records that are being archived. + +The actions are normal actions, similar to the [Delete](https://filamentphp.com/docs/3.x/actions/prebuilt-actions/delete) and [Restore](https://filamentphp.com/docs/3.x/actions/prebuilt-actions/restore) actions of FilamentPHP. You can add all features that are described in the [FilamentPHP Actions Documentation](https://filamentphp.com/docs/3.x/actions/overview), like: + +- ```color()``` +- ```size()``` +- ```disabled()``` +- ```icon()``` +- ... etc. + +```php +use Filament\Support\Enums\ActionSize; + +ArchiveAction::make() + ->color('success') + ->size(ActionSize::Large), +``` ### Filtering @@ -122,6 +164,26 @@ public static function table(Table $table): Table The ```ArchivedFilter``` will respect all options that Tenary Filters have, so check the [Tenary Filter Documentation of Filament](https://filamentphp.com/docs/3.x/tables/filters/ternary) to customize the filter. +#### Ability to view/edit/delete archived records +If you want to be able to view/edit/delete archived records, you should disable the global ```ArchivedScope::class``` on your resource ``` getEloquentQuery()``` method: + +```php +// in your resource: +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\SoftDeletingScope; +use LaravelArchivable\Scopes\ArchivableScope; + +public static function getEloquentQuery(): Builder +{ + return parent::getEloquentQuery() + ->withoutGlobalScopes([ + SoftDeletingScope::class, // only if soft deleting is also active, otherwise it can be ommitted + ArchivableScope::class, + ]); +} +``` +See [Disabeling global scopes on Filament](https://filamentphp.com/docs/3.x/panels/resources/getting-started#disabling-global-scopes) for more information about the default resource query. + ### Add custom classes to archived rows This plugin comes with a ```Table::macro``` which allows you to add custom (CSS/Tailwind) classes to ***table-rows** that are archived*: diff --git a/resources/lang/en/actions.php b/resources/lang/en/actions.php new file mode 100644 index 0000000..11b0717 --- /dev/null +++ b/resources/lang/en/actions.php @@ -0,0 +1,65 @@ + [ + + 'single' => [ + + 'label' => 'Archive', + + 'modal' => [ + + 'heading' => 'Archive :label', + + 'actions' => [ + + 'archive' => [ + + 'label' => 'Archive', + ], + + ], + + ], + + 'notifications' => [ + + 'archived' => [ + + 'title' => 'Record archived', + ], + ], + ], + ], + + 'unarchive' => [ + + 'single' => [ + + 'label' => 'Unarchive', + + 'modal' => [ + + 'heading' => 'Unarchive :label', + + 'actions' => [ + + 'unarchive' => [ + + 'label' => 'Unarchive', + ], + + ], + ], + + 'notifications' => [ + + 'unarchived' => [ + + 'title' => 'Record unarchived', + ], + ], + ], + ], +]; diff --git a/resources/lang/nl/actions.php b/resources/lang/nl/actions.php new file mode 100644 index 0000000..7e43504 --- /dev/null +++ b/resources/lang/nl/actions.php @@ -0,0 +1,65 @@ + [ + + 'single' => [ + + 'label' => 'Archiveer', + + 'modal' => [ + + 'heading' => 'Archiveer :label', + + 'actions' => [ + + 'archive' => [ + + 'label' => 'Archiveren', + ], + + ], + + ], + + 'notifications' => [ + + 'archived' => [ + + 'title' => 'Record gearchiveerd', + ], + ], + ], + ], + + 'unarchive' => [ + + 'single' => [ + + 'label' => 'Herstellen', + + 'modal' => [ + + 'heading' => 'Herstel :label', + + 'actions' => [ + + 'unarchive' => [ + + 'label' => 'Herstellen', + ], + + ], + ], + + 'notifications' => [ + + 'unarchived' => [ + + 'title' => 'Record hersteld', + ], + ], + ], + ], +]; diff --git a/src/Actions/ArchiveAction.php b/src/Actions/ArchiveAction.php new file mode 100644 index 0000000..f454192 --- /dev/null +++ b/src/Actions/ArchiveAction.php @@ -0,0 +1,62 @@ +label(__('filament-archivable::actions.archive.single.label')); + + $this->modalHeading(fn (): string => __('filament-archivable::actions.archive.single.modal.heading', ['label' => $this->getRecordTitle()])); + + $this->modalSubmitActionLabel(__('filament-archivable::actions.archive.single.modal.actions.archived.label')); + + $this->successNotificationTitle(__('filament-archivable::actions.archive.single.notifications.archived.title')); + + $this->color('warning'); + + $this->icon('heroicon-m-archive-box-arrow-down'); + + $this->requiresConfirmation(); + + $this->modalIcon('heroicon-m-archive-box-arrow-down'); + + $this->hidden(static function (Model $record): bool { + if (! method_exists($record, 'isArchived')) { + // @codeCoverageIgnoreStart + return false; + // @codeCoverageIgnoreEnd + } + + return $record->isArchived(); + }); + + $this->action(function (): void { + $result = $this->process(static fn (Model $record) => $record->archive()); + + if (! $result) { + // @codeCoverageIgnoreStart + $this->failure(); + + return; + // @codeCoverageIgnoreEnd + } + + $this->success(); + }); + } +} diff --git a/src/Actions/UnArchiveAction.php b/src/Actions/UnArchiveAction.php new file mode 100644 index 0000000..be33d4d --- /dev/null +++ b/src/Actions/UnArchiveAction.php @@ -0,0 +1,70 @@ +label(__('filament-archivable::actions.unarchive.single.label')); + + $this->modalHeading(fn (): string => __('filament-archivable::actions.unarchive.single.modal.heading', ['label' => $this->getRecordTitle()])); + + $this->modalSubmitActionLabel(__('filament-archivable::actions.unarchive.single.modal.actions.unarchive.label')); + + $this->successNotificationTitle(__('filament-archivable::actions.unarchive.single.notifications.unarchived.title')); + + $this->color('gray'); + + $this->icon('heroicon-m-arrow-uturn-left'); + + $this->requiresConfirmation(); + + $this->modalIcon('heroicon-o-arrow-uturn-left'); + + $this->action(function (Model $record): void { + if (! method_exists($record, 'unArchive')) { + // @codeCoverageIgnoreStart + $this->failure(); + + return; + // @codeCoverageIgnoreEnd + } + + $result = $this->process(static fn () => $record->unArchive()); + + if (! $result) { + // @codeCoverageIgnoreStart + $this->failure(); + + return; + // @codeCoverageIgnoreEnd + } + + $this->success(); + }); + + $this->visible(static function (Model $record): bool { + if (! method_exists($record, 'isArchived')) { + // @codeCoverageIgnoreStart + return false; + // @codeCoverageIgnoreEnd + } + + return $record->isArchived(); + }); + } +} diff --git a/tests/FilamentArchivableTest.php b/tests/FilamentArchivableTest.php index 5137149..5d405b0 100644 --- a/tests/FilamentArchivableTest.php +++ b/tests/FilamentArchivableTest.php @@ -1,5 +1,7 @@ getId()) ->toBe('archivable'); @@ -178,7 +180,7 @@ }); it('can set default archived-table-row classes', function () { - $plugin = new FilamentArchivablePlugin(); + $plugin = new FilamentArchivablePlugin; expect(FilamentArchivable::$archivedRecordClasses) ->toBeNull(); @@ -222,7 +224,7 @@ it('can ignore default archived-table-row classes when specificly defined on the resource-table', function () { - $plugin = new FilamentArchivablePlugin(); + $plugin = new FilamentArchivablePlugin; expect(FilamentArchivable::$archivedRecordClasses) ->toBeNull(); @@ -247,3 +249,78 @@ ->assertDontSee('bg-red-300'); }); + +it('can show archive Action on Edit page', function () { + + $unArchivedModels = ModelWithArchivableTrait::factory()->count(1)->create(['archived_at' => null]); + + livewire(ModelWithArchivableTraitResource\Pages\EditPage::class, ['record' => $unArchivedModels->first()->getKey()]) + ->assertSuccessful() + ->assertActionExists(ActionsArchiveAction::class) + ->assertActionVisible(ActionsArchiveAction::class); + +}); + +it('can show unarchive Action on Edit page', function () { + + $archivedModels = ModelWithArchivableTrait::factory()->count(1)->create(['archived_at' => now()]); + + livewire(ModelWithArchivableTraitResource\Pages\EditPage::class, ['record' => $archivedModels->first()->getKey()]) + ->assertSuccessful() + ->assertActionExists(ActionsUnArchiveAction::class) + ->assertActionVisible(ActionsUnArchiveAction::class); + +}); + +it('does not show the unarchived action on a unarchived record', function () { + + $unArchivedModels = ModelWithArchivableTrait::factory()->count(1)->create(['archived_at' => null]); + + livewire(ModelWithArchivableTraitResource\Pages\EditPage::class, ['record' => $unArchivedModels->first()->getKey()]) + ->assertSuccessful() + ->assertActionExists(ActionsUnArchiveAction::class) + ->assertActionHidden(ActionsUnArchiveAction::class); + +}); + +it('does not show the archive action on a archived record', function () { + + $archivedModels = ModelWithArchivableTrait::factory()->count(1)->create(['archived_at' => now()]); + + livewire(ModelWithArchivableTraitResource\Pages\EditPage::class, ['record' => $archivedModels->first()->getKey()]) + ->assertSuccessful() + ->assertActionExists(ActionsArchiveAction::class) + ->assertActionHidden(ActionsArchiveAction::class); + +}); + +it('can archive a model on Edit page', function () { + + $unArchivedModel = ModelWithArchivableTrait::factory()->count(1)->create(['archived_at' => null])->first(); + + livewire(ModelWithArchivableTraitResource\Pages\EditPage::class, ['record' => $unArchivedModel->getKey()]) + ->assertSuccessful() + ->assertActionVisible(ActionsArchiveAction::class) + ->callAction(ActionsArchiveAction::class); + + $unArchivedModel->refresh(); + + $this-> + assertNotNull($unArchivedModel->archived_at); +}); + +it('can unarchive a model on Edit page', function () { + + $archivedModel = ModelWithArchivableTrait::factory()->count(1)->create(['archived_at' => now()])->first(); + + livewire(ModelWithArchivableTraitResource\Pages\EditPage::class, ['record' => $archivedModel->getKey()]) + ->assertSuccessful() + ->assertActionVisible(ActionsUnArchiveAction::class) + ->callAction(ActionsUnArchiveAction::class); + + $archivedModel->refresh(); + + $this-> + assertNull($archivedModel->archived_at); + +}); diff --git a/tests/TestResources/ModelWithArchivableTraitResource.php b/tests/TestResources/ModelWithArchivableTraitResource.php index b17a033..a48ef6a 100644 --- a/tests/TestResources/ModelWithArchivableTraitResource.php +++ b/tests/TestResources/ModelWithArchivableTraitResource.php @@ -8,10 +8,14 @@ use Filament\Resources\Resource; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\SoftDeletingScope; +use LaravelArchivable\Scopes\ArchivableScope; use Okeonline\FilamentArchivable\Tables\Actions\ArchiveAction; use Okeonline\FilamentArchivable\Tables\Actions\UnArchiveAction; use Okeonline\FilamentArchivable\Tables\Filters\ArchivedFilter; use Okeonline\FilamentArchivable\Tests\TestModels\ModelWithArchivableTrait; +use Okeonline\FilamentArchivable\Tests\TestResources\ModelWithArchivableTraitResource\Pages\EditPage; use Okeonline\FilamentArchivable\Tests\TestResources\ModelWithArchivableTraitResource\Pages\ListPage; class ModelWithArchivableTraitResource extends Resource @@ -51,6 +55,16 @@ public static function getPages(): array { return [ 'index' => ListPage::route('/'), + 'edit' => EditPage::route('/edit/{record}'), ]; } + + public static function getEloquentQuery(): Builder + { + return parent::getEloquentQuery() + ->withoutGlobalScopes([ + SoftDeletingScope::class, + ArchivableScope::class, + ]); + } } diff --git a/tests/TestResources/ModelWithArchivableTraitResource/Pages/EditPage.php b/tests/TestResources/ModelWithArchivableTraitResource/Pages/EditPage.php new file mode 100644 index 0000000..b43dfb0 --- /dev/null +++ b/tests/TestResources/ModelWithArchivableTraitResource/Pages/EditPage.php @@ -0,0 +1,21 @@ + Date: Fri, 9 Aug 2024 19:18:24 +0200 Subject: [PATCH 2/2] fix readme --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index eb6f66e..d5c6bb7 100644 --- a/README.md +++ b/README.md @@ -114,11 +114,9 @@ protected function getHeaderActions(): array It will show the ```ArchiveAction``` on records that aren't archived, and will show the ```UnArchiveAction``` on those which are currently archived: --->screenshot<-- - > Be aware that there is a difference between **table** actions and **normal (page)** actions. You can not use the page actions as a table action, vice versa. Nevertheless, the business-logic is the same. *Read [this page](https://filamentphp.com/docs/3.x/actions/overview) for more information about the difference.* -> You should add **both** actions to the same page. The action itself wil determine if it should be shown on the record. Check -->internal<--- if you want to edit (and unarchive) records that are being archived. +> You should add **both** actions to the same page. The action itself wil determine if it should be shown on the record. Check [this](#add-custom-classes-to-archived-rows) if you want to edit (and unarchive) records that are being archived. The actions are normal actions, similar to the [Delete](https://filamentphp.com/docs/3.x/actions/prebuilt-actions/delete) and [Restore](https://filamentphp.com/docs/3.x/actions/prebuilt-actions/restore) actions of FilamentPHP. You can add all features that are described in the [FilamentPHP Actions Documentation](https://filamentphp.com/docs/3.x/actions/overview), like: