WORKIGN COMMENTS
This commit is contained in:
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Modules\Helpdesk\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Routing\Controller;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Modules\Helpdesk\Models\Ticket;
|
||||||
|
use Modules\Helpdesk\Services\TicketService;
|
||||||
|
|
||||||
|
final class TicketCommentStoreController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Store a ticket comment using a normal HTTP POST request.
|
||||||
|
*/
|
||||||
|
public function __invoke(Request $request, Ticket $ticket, TicketService $tickets): RedirectResponse
|
||||||
|
{
|
||||||
|
Gate::authorize('comment', $ticket);
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'commentBody' => ['required', 'string', 'min:2'],
|
||||||
|
'attachments' => ['nullable', 'array'],
|
||||||
|
'attachments.*' => [
|
||||||
|
'file',
|
||||||
|
'max:'.config('helpdesk.max_upload_kb', 10240),
|
||||||
|
'mimes:'.implode(',', config('helpdesk.allowed_mimes', ['jpg', 'jpeg', 'png', 'pdf', 'txt', 'log', 'zip'])),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
if ($user === null) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'commentBody' => 'You must be signed in to comment on this ticket.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$internal = $request->user()?->can('update', $ticket) === true && $request->boolean('internal');
|
||||||
|
|
||||||
|
$tickets->comment(
|
||||||
|
ticket: $ticket,
|
||||||
|
user: $user,
|
||||||
|
body: (string) $validated['commentBody'],
|
||||||
|
internal: $internal,
|
||||||
|
files: $request->file('attachments', []),
|
||||||
|
);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('helpdesk.tickets.show', $ticket)
|
||||||
|
->with('ticket-comment-added', 'Reply posted.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,8 +80,14 @@
|
|||||||
@endif
|
@endif
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-700 dark:bg-zinc-900" dusk="ticket-comment-form">
|
<form method="POST" action="{{ route('helpdesk.tickets.comments.store', $ticket) }}" enctype="multipart/form-data" class="rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||||
|
@csrf
|
||||||
<h2 class="mb-4 text-base font-semibold text-zinc-900 dark:text-white">Add reply</h2>
|
<h2 class="mb-4 text-base font-semibold text-zinc-900 dark:text-white">Add reply</h2>
|
||||||
|
@if (session('ticket-comment-added'))
|
||||||
|
<div class="mb-4 rounded-xl border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm font-bold text-emerald-800 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-100">
|
||||||
|
{{ session('ticket-comment-added') }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
@if ($cannedResponses->isNotEmpty())
|
@if ($cannedResponses->isNotEmpty())
|
||||||
<div class="mb-4 grid gap-3 md:grid-cols-[1fr_auto]">
|
<div class="mb-4 grid gap-3 md:grid-cols-[1fr_auto]">
|
||||||
<flux:select wire:model="canned_response_id" label="Canned response">
|
<flux:select wire:model="canned_response_id" label="Canned response">
|
||||||
@@ -96,20 +102,7 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div class="space-y-2">
|
<x-helpdesk::html-editor dusk="ticket-comment-editor" wire:model="commentBody" label="Comment" rows="5" placeholder="Write a professional response..." />
|
||||||
<label for="ticket-comment-body" class="block text-sm font-medium text-zinc-700 dark:text-zinc-200">Comment</label>
|
|
||||||
<textarea
|
|
||||||
id="ticket-comment-body"
|
|
||||||
dusk="ticket-comment-body"
|
|
||||||
wire:model.live="commentBody"
|
|
||||||
rows="5"
|
|
||||||
placeholder="Write a professional response..."
|
|
||||||
class="block w-full rounded-xl border border-zinc-300 bg-white p-4 text-sm text-zinc-900 shadow-sm outline-none transition focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/30 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
|
|
||||||
></textarea>
|
|
||||||
@error('commentBody')
|
|
||||||
<div class="text-sm text-red-600">{{ $message }}</div>
|
|
||||||
@enderror
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@can('update', $ticket)
|
@can('update', $ticket)
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
@@ -119,25 +112,17 @@
|
|||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<label class="mb-2 block text-sm font-medium text-zinc-700 dark:text-zinc-200">Attach files</label>
|
<label class="mb-2 block text-sm font-medium text-zinc-700 dark:text-zinc-200">Attach files</label>
|
||||||
<input wire:model="attachments" type="file" multiple class="block w-full rounded-lg border border-zinc-300 bg-white p-2 text-sm dark:border-zinc-700 dark:bg-zinc-950" />
|
<input name="attachments[]" type="file" multiple class="block w-full rounded-lg border border-zinc-300 bg-white p-2 text-sm dark:border-zinc-700 dark:bg-zinc-950" />
|
||||||
@error('attachments.*') <div class="mt-2 text-sm text-red-600">{{ $message }}</div> @enderror
|
@error('attachments.*') <div class="mt-2 text-sm text-red-600">{{ $message }}</div> @enderror
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-5 flex justify-end">
|
<div class="mt-5 flex justify-end">
|
||||||
<button
|
<button dusk="post-comment" type="submit" class="inline-flex items-center justify-center rounded-xl bg-emerald-600 px-4 py-2 text-sm font-bold text-white shadow-sm transition hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-400/70">
|
||||||
dusk="post-comment"
|
|
||||||
type="button"
|
|
||||||
wire:click.prevent="addComment"
|
|
||||||
wire:target="addComment"
|
|
||||||
wire:loading.attr="disabled"
|
|
||||||
class="inline-flex items-center justify-center rounded-xl bg-emerald-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-400/60 disabled:cursor-not-allowed disabled:opacity-70"
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-comments mr-2" aria-hidden="true"></i>
|
<i class="fa-solid fa-comments mr-2" aria-hidden="true"></i>
|
||||||
<span wire:loading.remove wire:target="addComment">Post reply</span>
|
<span>Post reply</span>
|
||||||
<span wire:loading wire:target="addComment">Posting...</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside class="space-y-6">
|
<aside class="space-y-6">
|
||||||
|
|||||||
@@ -43,31 +43,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (($reply_parent_id ?? null) === $comment->id)
|
@if (($reply_parent_id ?? null) === $comment->id)
|
||||||
<div class="mt-4 rounded-xl border border-blue-200 bg-blue-50 p-4 dark:border-blue-900 dark:bg-blue-950/40">
|
<form wire:submit="addReply" class="mt-4 rounded-xl border border-blue-200 bg-blue-50 p-4 dark:border-blue-900 dark:bg-blue-950/40">
|
||||||
<div class="space-y-2">
|
<x-helpdesk::html-editor wire:key="reply-editor-{{ $comment->id }}" wire:model="replyBody" label="Reply" rows="4" placeholder="Write a reply..." />
|
||||||
<label for="ticket-reply-body-{{ $comment->id }}" class="block text-sm font-medium text-zinc-700 dark:text-zinc-200">Reply</label>
|
|
||||||
<textarea
|
|
||||||
id="ticket-reply-body-{{ $comment->id }}"
|
|
||||||
dusk="ticket-reply-body"
|
|
||||||
wire:key="reply-editor-{{ $comment->id }}"
|
|
||||||
wire:model.live="replyBody"
|
|
||||||
rows="4"
|
|
||||||
placeholder="Write a reply..."
|
|
||||||
class="block w-full rounded-xl border border-zinc-300 bg-white p-4 text-sm text-zinc-900 shadow-sm outline-none transition focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/30 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
|
|
||||||
></textarea>
|
|
||||||
@error('replyBody')
|
|
||||||
<div class="text-sm text-red-600">{{ $message }}</div>
|
|
||||||
@enderror
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4 flex justify-end gap-2">
|
<div class="mt-4 flex justify-end gap-2">
|
||||||
<button type="button" wire:click.prevent="cancelReply" class="inline-flex items-center justify-center rounded-xl bg-slate-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-400/60">Cancel</button>
|
<flux:button type="button" wire:click="cancelReply" variant="ghost" class="!bg-slate-600 !text-white hover:!bg-slate-700 focus:!ring-slate-400/60">Cancel</flux:button>
|
||||||
<button type="button" wire:click.prevent="addReply" wire:target="addReply" wire:loading.attr="disabled" class="inline-flex items-center justify-center rounded-xl bg-emerald-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-400/60 disabled:cursor-not-allowed disabled:opacity-70">
|
<flux:button type="submit" variant="primary" class="!bg-emerald-600 !text-white hover:!bg-emerald-700 focus:!ring-emerald-400/60">Post reply</flux:button>
|
||||||
<span wire:loading.remove wire:target="addReply">Post reply</span>
|
|
||||||
<span wire:loading wire:target="addReply">Posting...</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if ($commentReplies->isNotEmpty())
|
@if ($commentReplies->isNotEmpty())
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Route;
|
|||||||
use Modules\Helpdesk\Http\Controllers\AttachmentDownloadController;
|
use Modules\Helpdesk\Http\Controllers\AttachmentDownloadController;
|
||||||
use Modules\Helpdesk\Http\Controllers\HelpdeskController;
|
use Modules\Helpdesk\Http\Controllers\HelpdeskController;
|
||||||
use Modules\Helpdesk\Http\Controllers\TicketControlsController;
|
use Modules\Helpdesk\Http\Controllers\TicketControlsController;
|
||||||
|
use Modules\Helpdesk\Http\Controllers\TicketCommentStoreController;
|
||||||
use Modules\Helpdesk\Http\Controllers\TicketStoreController;
|
use Modules\Helpdesk\Http\Controllers\TicketStoreController;
|
||||||
use Modules\Helpdesk\Http\Controllers\WorkflowOpenController;
|
use Modules\Helpdesk\Http\Controllers\WorkflowOpenController;
|
||||||
use Modules\Helpdesk\Livewire\KanbanBoard;
|
use Modules\Helpdesk\Livewire\KanbanBoard;
|
||||||
@@ -29,6 +30,7 @@ Route::get('/tickets/{ticket}/edit', TicketEdit::class)->name('tickets.edit');
|
|||||||
Route::post('/tickets/{ticket}/agent', [TicketControlsController::class, 'updateAgent'])->name('tickets.agent.update');
|
Route::post('/tickets/{ticket}/agent', [TicketControlsController::class, 'updateAgent'])->name('tickets.agent.update');
|
||||||
Route::post('/tickets/{ticket}/status', [TicketControlsController::class, 'updateStatus'])->name('tickets.status.update');
|
Route::post('/tickets/{ticket}/status', [TicketControlsController::class, 'updateStatus'])->name('tickets.status.update');
|
||||||
Route::post('/tickets/{ticket}/priority', [TicketControlsController::class, 'updatePriority'])->name('tickets.priority.update');
|
Route::post('/tickets/{ticket}/priority', [TicketControlsController::class, 'updatePriority'])->name('tickets.priority.update');
|
||||||
|
Route::post('/tickets/{ticket}/comments', TicketCommentStoreController::class)->name('tickets.comments.store');
|
||||||
Route::get('/tickets/{ticket}', TicketShow::class)->name('tickets.show');
|
Route::get('/tickets/{ticket}', TicketShow::class)->name('tickets.show');
|
||||||
Route::get('/attachments/{attachment}/download', AttachmentDownloadController::class)
|
Route::get('/attachments/{attachment}/download', AttachmentDownloadController::class)
|
||||||
->whereNumber('attachment')
|
->whereNumber('attachment')
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace Modules\Helpdesk\Tests\Browser;
|
namespace Modules\Helpdesk\Tests\Browser;
|
||||||
|
|
||||||
use Laravel\Dusk\Browser;
|
use Laravel\Dusk\Browser;
|
||||||
|
use Modules\Helpdesk\Models\Comment;
|
||||||
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
|
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
|
||||||
use Tests\DuskTestCase;
|
use Tests\DuskTestCase;
|
||||||
|
|
||||||
@@ -60,20 +61,25 @@ final class HelpdeskTicketBrowserTest extends DuskTestCase
|
|||||||
$browser->loginAs($user)
|
$browser->loginAs($user)
|
||||||
->visitRoute('helpdesk.tickets.show', $ticket)
|
->visitRoute('helpdesk.tickets.show', $ticket)
|
||||||
->waitFor('@helpdesk-ticket-show')
|
->waitFor('@helpdesk-ticket-show')
|
||||||
->assertSee('Browser response ticket')
|
->assertSee('Browser response ticket');
|
||||||
->type('@ticket-comment-body', 'First browser response')
|
|
||||||
|
$this->setHelpdeskEditor($browser, 'ticket-comment-editor', '<p>First browser response</p>');
|
||||||
|
|
||||||
|
$browser->pause(1000)
|
||||||
->press('@post-comment')
|
->press('@post-comment')
|
||||||
|
->waitForLocation(route('helpdesk.tickets.show', $ticket, false), 10)
|
||||||
->waitForText('First browser response', 10)
|
->waitForText('First browser response', 10)
|
||||||
->assertSee('First browser response');
|
->assertSee('First browser response');
|
||||||
});
|
|
||||||
|
|
||||||
$this->assertDatabaseHas('helpdesk_comments', [
|
$this->assertDatabaseHas('helpdesk_comments', [
|
||||||
'ticket_id' => $ticket->id,
|
'ticket_id' => $ticket->id,
|
||||||
'parent_id' => null,
|
'user_id' => $user->id,
|
||||||
'user_id' => $user->id,
|
'body' => '<p>First browser response</p>',
|
||||||
'body' => 'First browser response',
|
'is_internal' => false,
|
||||||
'is_internal' => false,
|
]);
|
||||||
]);
|
|
||||||
|
self::assertSame(1, Comment::query()->where('ticket_id', $ticket->id)->count());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private function setHelpdeskEditor(Browser $browser, string $dusk, string $html): void
|
private function setHelpdeskEditor(Browser $browser, string $dusk, string $html): void
|
||||||
|
|||||||
@@ -205,10 +205,11 @@ versions:
|
|||||||
- Replaced browser-test Livewire internals with real UI interactions for settings sections, Select2 controls and HTML editors.
|
- Replaced browser-test Livewire internals with real UI interactions for settings sections, Select2 controls and HTML editors.
|
||||||
- Kept module.json version in sync with changelog.yaml.
|
- Kept module.json version in sync with changelog.yaml.
|
||||||
|
|
||||||
|
|
||||||
"1.3.12":
|
"1.3.12":
|
||||||
changed:
|
changed:
|
||||||
- Fixed ticket comment submission by forcing the Helpdesk HTML editor content into Livewire before directly invoking addComment from the form submit path.
|
- Fixed ticket comment submission by adding a CSRF-protected native POST endpoint that stores comments through TicketService when Livewire or Flux submit handling does not call addComment.
|
||||||
- Fixed nested reply submission by applying the same editor-to-Livewire synchronisation before addReply.
|
- Updated the ticket detail reply form to post directly to the comment endpoint and refresh the conversation from the database after redirect.
|
||||||
- Updated the Helpdesk browser test so comment creation must persist a real helpdesk_comments database row instead of being masked by direct Livewire component mutation.
|
- Hardened the Helpdesk HTML editor so native form submissions synchronise the latest editor content before the browser posts the form.
|
||||||
|
- Added feature and browser coverage proving ticket comments are created in helpdesk_comments and rendered back on the ticket detail page.
|
||||||
- Kept module.json version in sync with changelog.yaml.
|
- Kept module.json version in sync with changelog.yaml.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user