Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions resources/lang/ar/comments.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

'cancel' => 'إلغاء',
'delete' => 'حذف',
'edit' => 'تعديل',
'save' => 'حفظ',
'comment' => 'تعليق',
'add_reaction' => 'إضافة رد فعل',
Expand Down
1 change: 1 addition & 0 deletions resources/lang/en/comments.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

'cancel' => 'Cancel',
'delete' => 'Delete',
'edit' => 'Edit',
'save' => 'Save',
'comment' => 'Comment',
'add_reaction' => 'Add Reaction',
Expand Down
1 change: 1 addition & 0 deletions resources/lang/es/comments.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

'cancel' => 'Cancelar',
'delete' => 'Eliminar',
'edit' => 'Editar',
'save' => 'Guardar',
'comment' => 'Comentar',
'add_reaction' => 'Agregar reacción',
Expand Down
1 change: 1 addition & 0 deletions resources/lang/fr/comments.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

'cancel' => 'Annuler',
'delete' => 'Supprimer',
'edit' => 'Modifier',
'save' => 'Enregistrer',
'comment' => 'Commenter',
'add_reaction' => 'Ajouter une réaction',
Expand Down
1 change: 1 addition & 0 deletions resources/lang/nl/comments.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

'cancel' => 'Annuleren',
'delete' => 'Verwijderen',
'edit' => 'Bewerken',
'save' => 'Opslaan',
'comment' => 'Opmerking',
'add_reaction' => 'Reactie toevoegen',
Expand Down
1 change: 1 addition & 0 deletions resources/lang/ro/comments.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

'cancel' => 'Anulare',
'delete' => 'Șterge',
'edit' => 'Editează',
'save' => 'Salvare',
'comment' => 'Comentariu',
'add_reaction' => 'Adaugă reacție',
Expand Down
59 changes: 8 additions & 51 deletions resources/views/comment.blade.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
@use('\Kirschbaum\Commentions\Config')

<div class="comm:flex comm:items-start comm:gap-x-4 comm:border comm:border-gray-300 comm:dark:border-gray-700 comm:p-4 comm:rounded-lg comm:shadow-sm comm:mb-2" id="filament-comment-{{ $comment->getId() }}">
@if ($avatar = $comment->getAuthorAvatar())
<img
Expand Down Expand Up @@ -34,57 +32,14 @@ class="comm:text-xs comm:text-gray-300 comm:ml-1"
@endif
</div>

@if ($comment->isComment() && Config::resolveAuthenticatedUser()?->canAny(['update', 'delete'], $comment))
@if ($comment->isComment())
<div class="comm:flex comm:gap-x-1">
@if (Config::resolveAuthenticatedUser()?->can('update', $comment))
<x-filament::icon-button
icon="heroicon-s-pencil-square"
wire:click="edit"
size="xs"
color="gray"
/>
@endif

@if (Config::resolveAuthenticatedUser()?->can('delete', $comment))
<x-filament::modal
id="delete-comment-modal-{{ $comment->getId() }}"
width="sm"
>
<x-slot name="trigger">
<x-filament::icon-button
icon="heroicon-s-trash"
size="xs"
color="gray"
/>
</x-slot>

<x-slot name="heading">
{{ __('commentions::comments.delete_comment_heading') }}
</x-slot>
{{ $this->editAction }}
{{ $this->deleteAction }}

<div class="comm:py-4">
{{ __('commentions::comments.delete_comment_body') }}
</div>

<x-slot name="footer">
<div class="comm:flex comm:justify-end comm:gap-x-4">
<x-filament::button
wire:click="$dispatch('close-modal', { id: 'delete-comment-modal-{{ $comment->getId() }}' })"
color="gray"
>
{{ __('commentions::comments.cancel') }}
</x-filament::button>

<x-filament::button
wire:click="delete"
color="danger"
>
{{ __('commentions::comments.delete') }}
</x-filament::button>
</div>
</x-slot>
</x-filament::modal>
@endif
@foreach ($this->getCustomActions() as $commentAction)
{{ $commentAction }}
@endforeach
</div>
@endif
</div>
Expand Down Expand Up @@ -126,4 +81,6 @@ class="comm:text-xs comm:text-gray-300 comm:ml-1"
@endif
@endif
</div>

<x-filament-actions::modals />
</div>
35 changes: 35 additions & 0 deletions src/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use Closure;
use Composer\InstalledVersions;
use Filament\Actions\Action;
use Illuminate\Support\Arr;
use InvalidArgumentException;
use Kirschbaum\Commentions\Contracts\Commenter;

Expand All @@ -17,6 +19,9 @@ class Config

protected static ?Closure $resolveTipTapCssClasses = null;

/** @var array<Closure> */
protected static array $commentActions = [];

public static function resolveAuthenticatedUserUsing(Closure $callback): void
{
static::$resolveAuthenticatedUser = $callback;
Expand Down Expand Up @@ -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<Action>) $callback
*/
public static function registerCommentActions(Closure $callback): void
{
static::$commentActions[] = $callback;
}

/**
* Resolve the custom actions registered for a given comment.
*
* @return array<Action>
*/
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::';
Expand Down
12 changes: 11 additions & 1 deletion src/Livewire/Comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
81 changes: 81 additions & 0 deletions src/Livewire/Concerns/HasCommentActions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

namespace Kirschbaum\Commentions\Livewire\Concerns;

use Filament\Actions\Action;
use Kirschbaum\Commentions\Comment as CommentModel;
use Kirschbaum\Commentions\Config;
use Kirschbaum\Commentions\Livewire\Comment;

/**
* Provides the built-in edit/delete Filament actions for a comment, and caches
* any host-registered custom actions so they remain resolvable across requests.
*
* @mixin Comment
*/
trait HasCommentActions
{
/** @var array<Action>|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<Action>
*/
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);
}
}
90 changes: 90 additions & 0 deletions tests/Filament/CommentCustomActionsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

use Filament\Actions\Action;
use Illuminate\Support\Facades\Auth;
use Kirschbaum\Commentions\Comment as CommentModel;
use Kirschbaum\Commentions\Config;
use Kirschbaum\Commentions\Livewire\Comment as CommentComponent;
use Kirschbaum\Commentions\RenderableComment;
use Tests\Models\Post;
use Tests\Models\User;

use function Pest\Laravel\actingAs;
use function Pest\Livewire\livewire;

beforeEach(function () {
Config::resolveAuthenticatedUserUsing(fn () => 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');
});
Loading
Loading