diff --git a/resources/lang/ar/comments.php b/resources/lang/ar/comments.php index ddf3e4e..119284d 100644 --- a/resources/lang/ar/comments.php +++ b/resources/lang/ar/comments.php @@ -16,6 +16,7 @@ 'cancel' => 'إلغاء', 'delete' => 'حذف', + 'edit' => 'تعديل', 'save' => 'حفظ', 'comment' => 'تعليق', 'add_reaction' => 'إضافة رد فعل', diff --git a/resources/lang/en/comments.php b/resources/lang/en/comments.php index 77dc525..93e2067 100644 --- a/resources/lang/en/comments.php +++ b/resources/lang/en/comments.php @@ -16,6 +16,7 @@ 'cancel' => 'Cancel', 'delete' => 'Delete', + 'edit' => 'Edit', 'save' => 'Save', 'comment' => 'Comment', 'add_reaction' => 'Add Reaction', diff --git a/resources/lang/es/comments.php b/resources/lang/es/comments.php index 99d02de..be643cf 100644 --- a/resources/lang/es/comments.php +++ b/resources/lang/es/comments.php @@ -16,6 +16,7 @@ 'cancel' => 'Cancelar', 'delete' => 'Eliminar', + 'edit' => 'Editar', 'save' => 'Guardar', 'comment' => 'Comentar', 'add_reaction' => 'Agregar reacción', diff --git a/resources/lang/fr/comments.php b/resources/lang/fr/comments.php index ea38080..b593974 100644 --- a/resources/lang/fr/comments.php +++ b/resources/lang/fr/comments.php @@ -16,6 +16,7 @@ 'cancel' => 'Annuler', 'delete' => 'Supprimer', + 'edit' => 'Modifier', 'save' => 'Enregistrer', 'comment' => 'Commenter', 'add_reaction' => 'Ajouter une réaction', diff --git a/resources/lang/nl/comments.php b/resources/lang/nl/comments.php index 187446a..bda6fde 100644 --- a/resources/lang/nl/comments.php +++ b/resources/lang/nl/comments.php @@ -15,6 +15,7 @@ 'cancel' => 'Annuleren', 'delete' => 'Verwijderen', + 'edit' => 'Bewerken', 'save' => 'Opslaan', 'comment' => 'Opmerking', 'add_reaction' => 'Reactie toevoegen', diff --git a/resources/lang/ro/comments.php b/resources/lang/ro/comments.php index a3c0ff4..35167a3 100644 --- a/resources/lang/ro/comments.php +++ b/resources/lang/ro/comments.php @@ -16,6 +16,7 @@ 'cancel' => 'Anulare', 'delete' => 'Șterge', + 'edit' => 'Editează', 'save' => 'Salvare', 'comment' => 'Comentariu', 'add_reaction' => 'Adaugă reacție', diff --git a/resources/views/comment.blade.php b/resources/views/comment.blade.php index 8c880f3..d7157dc 100644 --- a/resources/views/comment.blade.php +++ b/resources/views/comment.blade.php @@ -1,5 +1,3 @@ -@use('\Kirschbaum\Commentions\Config') -
@if ($avatar = $comment->getAuthorAvatar()) - @if ($comment->isComment() && Config::resolveAuthenticatedUser()?->canAny(['update', 'delete'], $comment)) + @if ($comment->isComment())
- @if (Config::resolveAuthenticatedUser()?->can('update', $comment)) - - @endif - - @if (Config::resolveAuthenticatedUser()?->can('delete', $comment)) - - - - - - - {{ __('commentions::comments.delete_comment_heading') }} - + {{ $this->editAction }} + {{ $this->deleteAction }} -
- {{ __('commentions::comments.delete_comment_body') }} -
- - -
- - {{ __('commentions::comments.cancel') }} - - - - {{ __('commentions::comments.delete') }} - -
-
-
- @endif + @foreach ($this->getCustomActions() as $commentAction) + {{ $commentAction }} + @endforeach
@endif
@@ -126,4 +81,6 @@ class="comm:text-xs comm:text-gray-300 comm:ml-1" @endif @endif + + diff --git a/src/Config.php b/src/Config.php index 052a2e2..9722fd3 100644 --- a/src/Config.php +++ b/src/Config.php @@ -4,6 +4,8 @@ use Closure; use Composer\InstalledVersions; +use Filament\Actions\Action; +use Illuminate\Support\Arr; use InvalidArgumentException; use Kirschbaum\Commentions\Contracts\Commenter; @@ -17,6 +19,9 @@ class Config protected static ?Closure $resolveTipTapCssClasses = null; + /** @var array */ + protected static array $commentActions = []; + public static function resolveAuthenticatedUserUsing(Closure $callback): void { static::$resolveAuthenticatedUser = $callback; @@ -96,6 +101,36 @@ public static function getTipTapCssClasses(): ?string return 'comm:prose comm:dark:prose-invert comm:prose-sm comm:sm:prose-base comm:lg:prose-lg comm:xl:prose-2xl comm:focus:outline-none comm:p-4 comm:min-w-full comm:w-full comm:rounded-lg comm:border comm:border-gray-300 comm:dark:border-gray-700'; } + /** + * Register a callback that returns one or more Filament actions to render + * alongside each comment's built-in edit/delete actions. The callback + * receives the Comment the actions are being rendered for. + * + * @param Closure(Comment): (Action|array) $callback + */ + public static function registerCommentActions(Closure $callback): void + { + static::$commentActions[] = $callback; + } + + /** + * Resolve the custom actions registered for a given comment. + * + * @return array + */ + public static function getCommentActions(Comment $comment): array + { + return collect(static::$commentActions) + ->flatMap(fn (Closure $callback): array => Arr::wrap($callback($comment))) + ->values() + ->all(); + } + + public static function flushCommentActions(): void + { + static::$commentActions = []; + } + public static function getComponentPrefix(): string { return static::isLivewireV4() ? 'commentions.' : 'commentions::'; diff --git a/src/Livewire/Comment.php b/src/Livewire/Comment.php index 82eb5e9..7c5afc4 100644 --- a/src/Livewire/Comment.php +++ b/src/Livewire/Comment.php @@ -2,19 +2,29 @@ namespace Kirschbaum\Commentions\Livewire; +use Filament\Actions\Concerns\InteractsWithActions; +use Filament\Actions\Contracts\HasActions; use Filament\Notifications\Notification; +use Filament\Schemas\Concerns\InteractsWithSchemas; +use Filament\Schemas\Concerns\ResolvesDynamicLivewireProperties; +use Filament\Schemas\Contracts\HasSchemas; use Illuminate\Contracts\View\View; use Kirschbaum\Commentions\Comment as CommentModel; use Kirschbaum\Commentions\Config; use Kirschbaum\Commentions\Contracts\RenderableComment; +use Kirschbaum\Commentions\Livewire\Concerns\HasCommentActions; use Kirschbaum\Commentions\Livewire\Concerns\HasMentions; use Livewire\Attributes\On; use Livewire\Attributes\Renderless; use Livewire\Component; -class Comment extends Component +class Comment extends Component implements HasActions, HasSchemas { + use HasCommentActions; use HasMentions; + use InteractsWithActions; + use InteractsWithSchemas; + use ResolvesDynamicLivewireProperties; public CommentModel|RenderableComment $comment; diff --git a/src/Livewire/Concerns/HasCommentActions.php b/src/Livewire/Concerns/HasCommentActions.php new file mode 100644 index 0000000..e1b629d --- /dev/null +++ b/src/Livewire/Concerns/HasCommentActions.php @@ -0,0 +1,81 @@ +|null */ + protected ?array $customActions = null; + + /** + * Invoked by Filament's InteractsWithActions::cacheTraitActions() before + * mounted actions are resolved, so host-registered actions (including ones + * that open modals) stay resolvable on subsequent requests. + */ + public function cacheHasCommentActions(): void + { + foreach ($this->getCustomActions() as $action) { + $this->cacheAction($action); + } + } + + public function editAction(): Action + { + return Action::make('edit') + ->label(__('commentions::comments.edit')) + ->icon('heroicon-s-pencil-square') + ->iconButton() + ->size('xs') + ->color('gray') + ->visible(fn (): bool => $this->commentUserCan('update')) + ->action(fn () => $this->edit()); + } + + public function deleteAction(): Action + { + return Action::make('delete') + ->label(__('commentions::comments.delete')) + ->icon('heroicon-s-trash') + ->iconButton() + ->size('xs') + ->color('gray') + ->visible(fn (): bool => $this->commentUserCan('delete')) + ->requiresConfirmation() + ->modalHeading(__('commentions::comments.delete_comment_heading')) + ->modalDescription(__('commentions::comments.delete_comment_body')) + ->modalSubmitActionLabel(__('commentions::comments.delete')) + ->action(fn () => $this->delete()); + } + + /** + * Custom actions registered by the host application via + * Config::registerCommentActions(), rendered after edit/delete. + * + * @return array + */ + public function getCustomActions(): array + { + if (! $this->comment instanceof CommentModel) { + return []; + } + + return $this->customActions ??= Config::getCommentActions($this->comment); + } + + protected function commentUserCan(string $ability): bool + { + return $this->comment instanceof CommentModel + && (bool) Config::resolveAuthenticatedUser()?->can($ability, $this->comment); + } +} diff --git a/tests/Filament/CommentCustomActionsTest.php b/tests/Filament/CommentCustomActionsTest.php new file mode 100644 index 0000000..5a43f14 --- /dev/null +++ b/tests/Filament/CommentCustomActionsTest.php @@ -0,0 +1,90 @@ + Auth::user()); + Config::flushCommentActions(); +}); + +afterEach(function () { + Config::flushCommentActions(); +}); + +test('custom comment actions registered via Config are rendered', function () { + $user = User::factory()->create(); + actingAs($user); + + $post = Post::factory()->create(); + $comment = CommentModel::factory()->author($user)->commentable($post)->create(); + + Config::registerCommentActions(fn (CommentModel $comment) => Action::make('logs') + ->label('Activity Logs') + ->icon('heroicon-s-clock')); + + livewire(CommentComponent::class, ['comment' => $comment]) + ->assertActionVisible('logs') + ->assertSee('Activity Logs'); +}); + +test('custom comment actions can be called', function () { + $user = User::factory()->create(); + actingAs($user); + + $post = Post::factory()->create(); + $comment = CommentModel::factory()->author($user)->commentable($post)->create(); + + Config::registerCommentActions(fn (CommentModel $comment) => Action::make('ping') + ->action(fn () => $comment->update(['body' => 'pinged']))); + + livewire(CommentComponent::class, ['comment' => $comment]) + ->callAction('ping'); + + expect($comment->fresh()->body)->toBe('pinged'); +}); + +test('custom comment action callbacks receive the comment instance', function () { + $user = User::factory()->create(); + actingAs($user); + + $post = Post::factory()->create(); + $comment = CommentModel::factory()->author($user)->commentable($post)->create(); + + $received = null; + + Config::registerCommentActions(function (CommentModel $resolved) use (&$received) { + $received = $resolved; + + return Action::make('noop'); + }); + + livewire(CommentComponent::class, ['comment' => $comment]); + + expect($received)->not->toBeNull() + ->and($received->is($comment))->toBeTrue(); +}); + +test('custom comment actions are not rendered for non-comment renderables', function () { + $user = User::factory()->create(); + actingAs($user); + + Config::registerCommentActions(fn (CommentModel $comment) => Action::make('logs')->label('Activity Logs')); + + livewire(CommentComponent::class, [ + 'comment' => new RenderableComment( + id: 1, + authorName: 'System', + body: 'System notification', + ), + ])->assertActionDoesNotExist('logs'); +}); diff --git a/tests/Livewire/CommentTest.php b/tests/Livewire/CommentTest.php index 9b064c5..e82b0a2 100644 --- a/tests/Livewire/CommentTest.php +++ b/tests/Livewire/CommentTest.php @@ -29,8 +29,8 @@ ]) ->assertSee('Test comment body') ->assertSee($comment->author->name) - ->assertSeeHtml('wire:click="edit"') // Author should see an edit button - ->assertSeeHtml('wire:click="delete"'); // Author should see a delete button + ->assertActionVisible('edit') // Author should see an edit action + ->assertActionVisible('delete'); // Author should see a delete action }); test('other users cannot see edit and delete buttons by default', function () { @@ -44,8 +44,8 @@ livewire(CommentComponent::class, [ 'comment' => $comment, ]) - ->assertDontSeeHtml('wire:click="edit"') - ->assertDontSeeHtml('wire:click="delete"'); + ->assertActionHidden('edit') + ->assertActionHidden('delete'); }); test('guests cannot see edit and delete buttons', function () { @@ -56,8 +56,8 @@ livewire(CommentComponent::class, [ 'comment' => $comment, ]) - ->assertDontSeeHtml('wire:click="edit"') - ->assertDontSeeHtml('wire:click="delete"'); + ->assertActionHidden('edit') + ->assertActionHidden('delete'); }); test('custom policy can change who can see edit and delete buttons', function () { @@ -72,8 +72,8 @@ livewire(CommentComponent::class, [ 'comment' => $comment, ]) - ->assertDontSeeHtml('wire:click="edit"') - ->assertDontSeeHtml('wire:click="delete"'); + ->assertActionHidden('edit') + ->assertActionHidden('delete'); }); test('author can update a comment by default', function () { @@ -255,6 +255,41 @@ ]) ->assertSee('System notification') ->assertSee('System') - ->assertDontSeeHtml('wire:click="edit"') // Should not show edit button - ->assertDontSeeHtml('wire:click="delete"'); // Should not show delete button + ->assertActionHidden('edit') // Should not show edit action + ->assertActionHidden('delete'); // Should not show delete action +}); + +test('the edit action enters edit mode', function () { + $user = User::factory()->create(); + actingAs($user); + + $post = Post::factory()->create(); + $comment = CommentModel::factory()->author($user)->commentable($post)->create([ + 'body' => 'Test comment body', + ]); + + livewire(CommentComponent::class, [ + 'comment' => $comment, + ]) + ->assertSet('editing', false) + ->callAction('edit') + ->assertSet('editing', true); +}); + +test('the delete action removes the comment', function () { + $user = User::factory()->create(); + actingAs($user); + + $post = Post::factory()->create(); + $comment = CommentModel::factory()->author($user)->commentable($post)->create(); + + livewire(CommentComponent::class, [ + 'comment' => $comment, + ]) + ->callAction('delete') + ->assertDispatched('comment:deleted'); + + test()->assertDatabaseMissing('comments', [ + 'id' => $comment->id, + ]); }); diff --git a/tests/TestCase.php b/tests/TestCase.php index 226f557..e133512 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -4,7 +4,9 @@ use BladeUI\Heroicons\BladeHeroiconsServiceProvider; use BladeUI\Icons\BladeIconsServiceProvider; +use Filament\Actions\ActionsServiceProvider; use Filament\FilamentServiceProvider; +use Filament\Schemas\SchemasServiceProvider; use Filament\Support\SupportServiceProvider; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; @@ -33,6 +35,8 @@ protected function getPackageProviders($app) CommentionsServiceProvider::class, FilamentServiceProvider::class, SupportServiceProvider::class, + ActionsServiceProvider::class, + SchemasServiceProvider::class, LivewireServiceProvider::class, ]; }