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')
-
@@ -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,
];
}