diff --git a/app/Actions/Discussion/ConvertDiscussionToThreadAction.php b/app/Actions/Discussion/ConvertDiscussionToThreadAction.php new file mode 100644 index 00000000..44da90bf --- /dev/null +++ b/app/Actions/Discussion/ConvertDiscussionToThreadAction.php @@ -0,0 +1,36 @@ + $discussion->title, + 'slug' => $discussion->slug, + 'body' => $discussion->body, + 'user_id' => $discussion->user_id, + 'last_posted_at' => null, + ]); + + $discussion->replies()->update([ + 'replyable_type' => 'thread', + 'replyable_id' => $thread->id, + ]); + + $discussion->delete(); + + app(NotifyUsersOfThreadConversion::class)->execute($thread, $isAdmin); + + return $thread; + }); + } +} diff --git a/app/Actions/Discussion/NotifyUsersOfThreadConversion.php b/app/Actions/Discussion/NotifyUsersOfThreadConversion.php new file mode 100644 index 00000000..163ad914 --- /dev/null +++ b/app/Actions/Discussion/NotifyUsersOfThreadConversion.php @@ -0,0 +1,26 @@ +replies()->pluck('user_id')->unique()->toArray(); + + User::whereIn('id', $usersToNotify)->get()->each->notify(new ThreadConvertedByCreator($thread)); + + if ($isAdmin) { + $creator = $thread->user; + + $creator->notify(new ThreadConvertedByAdmin($thread)); + } + } +} diff --git a/app/Livewire/Modals/ConvertDiscussion.php b/app/Livewire/Modals/ConvertDiscussion.php new file mode 100644 index 00000000..6b323163 --- /dev/null +++ b/app/Livewire/Modals/ConvertDiscussion.php @@ -0,0 +1,33 @@ +discussionId); + + $this->authorize('convertedToThread', $discussion); + + $thread = app(ConvertDiscussionToThreadAction::class)->execute($discussion, Auth::user()->isAdmin()); + + $this->redirectRoute('forum.show', $thread, navigate: true); + } + + public function render(): View + { + return view('livewire.modals.convert-discussion'); + } +} diff --git a/app/Notifications/ThreadConvertedByAdmin.php b/app/Notifications/ThreadConvertedByAdmin.php new file mode 100644 index 00000000..efb076f8 --- /dev/null +++ b/app/Notifications/ThreadConvertedByAdmin.php @@ -0,0 +1,41 @@ + + */ + public function via(object $notifiable): array + { + return ['mail']; + } + + /** + * Get the mail representation of the notification. + */ + public function toMail(object $notifiable): MailMessage + { + return (new MailMessage) + ->subject('Discussion Converted by Admin') + ->greeting('Hello!') + ->line('An admin has converted a discussion to a thread.') + ->line('Thread Title: '.$this->thread->title) + ->action('View Thread', route('forum.show', $this->thread)) + ->line('This action was performed by an administrator.'); + } +} diff --git a/app/Notifications/ThreadConvertedByCreator.php b/app/Notifications/ThreadConvertedByCreator.php new file mode 100644 index 00000000..e9cd5661 --- /dev/null +++ b/app/Notifications/ThreadConvertedByCreator.php @@ -0,0 +1,40 @@ + + */ + public function via(object $notifiable): array + { + return ['mail']; + } + + /** + * Get the mail representation of the notification. + */ + public function toMail(object $notifiable): MailMessage + { + return (new MailMessage) + ->subject(__('Discussion Converted to Thread')) + ->line('A discussion you participated in has been converted to a thread.') + ->line('Thread Title: '.$this->thread->title) + ->action('View Thread', route('forum.show', $this->thread)) + ->line('Thank you for your participation!'); + } +} diff --git a/app/Policies/DiscussionPolicy.php b/app/Policies/DiscussionPolicy.php index 274518de..7ee28b4b 100644 --- a/app/Policies/DiscussionPolicy.php +++ b/app/Policies/DiscussionPolicy.php @@ -56,4 +56,9 @@ public function report(User $user, Discussion $discussion): bool { return $user->hasVerifiedEmail() && ! $discussion->isAuthoredBy($user); } + + public function convertedToThread(User $user, Discussion $discussion): bool + { + return $discussion->isAuthoredBy($user) || $user->isAdmin(); + } } diff --git a/lang/en/pages/discutssion.php b/lang/en/pages/discutssion.php new file mode 100644 index 00000000..cc1551b9 --- /dev/null +++ b/lang/en/pages/discutssion.php @@ -0,0 +1,23 @@ + 'Tous les sujets de discussion', + 'contributors' => [ + 'top' => 'Top Contributeurs', + 'description' => 'Les personnes qui ont lancé le plus de discussions sur le site.', + ], + 'empty' => 'Discussions sans commentaires', + 'empty_description' => 'Les discussions / sujets qui n’ont pas encore eu de commentaires. Soyez le premier à apporter votre contribution.', + 'total_answer' => 'total réponses', + 'new_discussion' => 'Nouveau discussion', + 'filter' => [ + 'recent' => 'Récent', + 'popular' => 'Populaire', + 'active' => 'Actif', + ], + 'comments_count' => 'Commentaires (:count)', + +]; diff --git a/resources/views/livewire/modals/convert-discussion.blade.php b/resources/views/livewire/modals/convert-discussion.blade.php new file mode 100644 index 00000000..ae92c468 --- /dev/null +++ b/resources/views/livewire/modals/convert-discussion.blade.php @@ -0,0 +1,35 @@ + + + {{ __("Confirmez la convertion") }} + + +
+
+
+ + {{ __("Voulez-vous vraiment convertir cette discussion en thread ?") }} + +
+
+
+ + + + + {{ __('Annuler') }} + + +
diff --git a/resources/views/livewire/pages/discussions/single-discussion.blade.php b/resources/views/livewire/pages/discussions/single-discussion.blade.php index e44bec21..bd935554 100644 --- a/resources/views/livewire/pages/discussions/single-discussion.blade.php +++ b/resources/views/livewire/pages/discussions/single-discussion.blade.php @@ -95,18 +95,27 @@ class="mx-auto mt-6 text-sm prose prose-sm prose-green max-w-none dark:prose-inv
{{ __('Éditer') }} · + @can('convertedToThread', $discussion) + · + + @endcan
@endcan diff --git a/tests/Feature/Actions/Discussion/ConvertDiscussionToThreadActionTest.php b/tests/Feature/Actions/Discussion/ConvertDiscussionToThreadActionTest.php new file mode 100644 index 00000000..8b0a1553 --- /dev/null +++ b/tests/Feature/Actions/Discussion/ConvertDiscussionToThreadActionTest.php @@ -0,0 +1,39 @@ +discussion = Discussion::factory()->create(); +}); + +describe(ConvertDiscussionToThreadAction::class, function (): void { + it('can converts a discussion to a thread', function (): void { + $replies = Reply::factory()->count(3)->create([ + 'replyable_type' => 'discussion', + 'replyable_id' => $this->discussion->id, + ]); + + $thread = app(ConvertDiscussionToThreadAction::class)->execute(discussion: $this->discussion); + + expect($thread)->toBeInstanceOf(Thread::class) + ->and(Discussion::find($this->discussion->id))->toBeNull(); + + $replies->each(function ($reply) use ($thread): void { + $updatedReply = Reply::find($reply->id); + + expect($updatedReply->replyable_type)->toBe('thread') + ->and($updatedReply->replyable_id)->toBe($thread->id); + }); + }); + + it('can handles admin conversion', function (): void { + $thread = app(ConvertDiscussionToThreadAction::class)->execute(discussion: $this->discussion, isAdmin: true); + + expect($thread)->toBeInstanceOf(Thread::class); + }); +}); diff --git a/tests/Feature/Livewire/Modal/ConvertDiscussionTest.php b/tests/Feature/Livewire/Modal/ConvertDiscussionTest.php new file mode 100644 index 00000000..aecdad17 --- /dev/null +++ b/tests/Feature/Livewire/Modal/ConvertDiscussionTest.php @@ -0,0 +1,29 @@ +create(); + $this->user = $this->actingAs($user); + $this->discussion = Discussion::factory()->create(); +}); + +describe(ConvertDiscussion::class, function (): void { + it('requires authorization to convert discussion', function (): void { + Livewire::test(ConvertDiscussion::class) + ->set('discussionId', $this->discussion->id) + ->call('save') + ->assertForbidden(); + }); + + it('throws exception for non-existent discussion', function (): void { + Livewire::test(ConvertDiscussion::class) + ->set('discussionId', 9) + ->call('save'); + })->throws(Illuminate\Database\Eloquent\ModelNotFoundException::class); +});