From c4646d2f5cc450af74ec818015f601f80c748d1a Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Wed, 17 Jun 2026 11:26:08 +0100 Subject: [PATCH] major updates --- ...11500_add_subject_to_helpdesk_comments.php | 53 ++++++++++++ .../TicketCommentStoreController.php | 35 +++++++- Modules/Helpdesk/Models/Comment.php | 1 + .../views/livewire/ticket-show.blade.php | 80 +++++++++++++++---- .../views/partials/comment-thread.blade.php | 62 ++++++++------ Modules/Helpdesk/Services/TicketService.php | 16 ++-- .../Browser/HelpdeskTicketBrowserTest.php | 43 ++++++++-- .../Tests/Feature/TicketConversationTest.php | 4 + Modules/Helpdesk/changelog.yaml | 11 +++ Modules/Helpdesk/module.json | 2 +- 10 files changed, 256 insertions(+), 51 deletions(-) create mode 100644 Modules/Helpdesk/Database/Migrations/2026_06_17_111500_add_subject_to_helpdesk_comments.php diff --git a/Modules/Helpdesk/Database/Migrations/2026_06_17_111500_add_subject_to_helpdesk_comments.php b/Modules/Helpdesk/Database/Migrations/2026_06_17_111500_add_subject_to_helpdesk_comments.php new file mode 100644 index 00000000..4e37817d --- /dev/null +++ b/Modules/Helpdesk/Database/Migrations/2026_06_17_111500_add_subject_to_helpdesk_comments.php @@ -0,0 +1,53 @@ +string('subject', 180)->nullable()->after('user_id'); + } + }); + + DB::table('helpdesk_comments') + ->whereNull('subject') + ->orWhere('subject', '') + ->orderBy('id') + ->select(['id', 'parent_id', 'body', 'body_text', 'is_internal']) + ->chunkById(100, static function ($comments): void { + foreach ($comments as $comment) { + $plain = trim(preg_replace('/\s+/', ' ', strip_tags((string) ($comment->body_text ?: $comment->body))) ?: ''); + $subject = (bool) $comment->is_internal + ? 'Internal note' + : ((int) ($comment->parent_id ?? 0) > 0 ? 'RE: ' : '').Str::limit($plain !== '' ? $plain : 'Response', 120, ''); + + DB::table('helpdesk_comments') + ->where('id', $comment->id) + ->update(['subject' => $subject]); + } + }); + + Schema::table('helpdesk_comments', static function (Blueprint $table): void { + $table->index(['ticket_id', 'subject'], 'helpdesk_comments_ticket_subject_index'); + }); + } + + public function down(): void + { + Schema::table('helpdesk_comments', static function (Blueprint $table): void { + if (Schema::hasColumn('helpdesk_comments', 'subject')) { + $table->dropIndex('helpdesk_comments_ticket_subject_index'); + $table->dropColumn('subject'); + } + }); + } +}; diff --git a/Modules/Helpdesk/Http/Controllers/TicketCommentStoreController.php b/Modules/Helpdesk/Http/Controllers/TicketCommentStoreController.php index 90a16fa1..75556d36 100644 --- a/Modules/Helpdesk/Http/Controllers/TicketCommentStoreController.php +++ b/Modules/Helpdesk/Http/Controllers/TicketCommentStoreController.php @@ -32,6 +32,7 @@ final class TicketCommentStoreController extends Controller $allowedMimes = (array) config('helpdesk.allowed_mimes', ['jpg', 'jpeg', 'png', 'pdf', 'txt', 'log', 'zip']); $validated = $request->validate([ + 'subject' => ['nullable', 'string', 'max:180'], 'commentBody' => ['nullable', 'string'], 'internal' => ['nullable', 'boolean'], 'parent_id' => [ @@ -48,16 +49,24 @@ final class TicketCommentStoreController extends Controller abort_if($user === null, 403); $body = trim((string) ($validated['commentBody'] ?? '')); + $subject = trim((string) ($validated['subject'] ?? '')); - if ($body === '' && filled($validated['canned_response_id'] ?? null)) { + if (filled($validated['canned_response_id'] ?? null)) { $response = CannedResponse::query() ->where('is_active', true) ->findOrFail((int) $validated['canned_response_id']); - $body = trim($renderer->render($response->body, $ticket, $user)); + if ($body === '') { + $body = trim($renderer->render($response->body, $ticket, $user)); + } + + if ($subject === '') { + $subject = (string) $response->name; + } } $body = $this->normaliseHtmlBody($body); + $subject = $this->normaliseSubject($subject, $body, filled($validated['parent_id'] ?? null)); validator(['commentBody' => $body], [ 'commentBody' => ['required', 'string', 'min:2'], @@ -73,12 +82,34 @@ final class TicketCommentStoreController extends Controller internal: (bool) ($validated['internal'] ?? false) && $this->userCanCreateInternalNote($user), files: $uploadedFiles, parentId: filled($validated['parent_id'] ?? null) ? (int) $validated['parent_id'] : null, + subject: $subject, ); return redirect() ->route('helpdesk.tickets.show', $ticket) ->with('ticket-comment-added', filled($validated['parent_id'] ?? null) ? 'Reply posted.' : 'Comment posted.'); } + + /** + * Derive a user-friendly Lotus Notes style response subject. + */ + private function normaliseSubject(string $subject, string $body, bool $isReply): string + { + $subject = trim(strip_tags(html_entity_decode($subject, ENT_QUOTES | ENT_HTML5, 'UTF-8'))); + + if ($subject !== '') { + return Str::limit($subject, 180, ''); + } + + $plain = trim(preg_replace('/\s+/', ' ', strip_tags(html_entity_decode($body, ENT_QUOTES | ENT_HTML5, 'UTF-8'))) ?: ''); + + if ($plain === '') { + return $isReply ? 'RE: Response' : 'Response'; + } + + return ($isReply ? 'RE: ' : '').Str::limit($plain, 120, ''); + } + /** * Keep supported formatting tags but strip scripts and unsafe inline handlers. */ diff --git a/Modules/Helpdesk/Models/Comment.php b/Modules/Helpdesk/Models/Comment.php index 20a1d4c5..6d33816a 100644 --- a/Modules/Helpdesk/Models/Comment.php +++ b/Modules/Helpdesk/Models/Comment.php @@ -18,6 +18,7 @@ final class Comment extends Model 'ticket_id', 'parent_id', 'user_id', + 'subject', 'body', 'body_text', 'is_internal', diff --git a/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php b/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php index 837e1ae3..1d3c1810 100644 --- a/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php +++ b/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php @@ -61,7 +61,7 @@
-
+
{!! $ticket->body !!}
@@ -101,16 +101,19 @@ @endif
-
- -
-

- - Conversation -

-

Lotus Notes-style document responses and responses to responses.

+
+ +
+ + + +
+

Conversation

+

Document responses

+

Lotus Notes-style document responses and replies.

+
- + {{ $visibleCommentCount }} item{{ $visibleCommentCount === 1 ? '' : 's' }} @@ -119,7 +122,7 @@
- +
diff --git a/Modules/Helpdesk/Services/TicketService.php b/Modules/Helpdesk/Services/TicketService.php index 549aa7df..5e2ea44b 100644 --- a/Modules/Helpdesk/Services/TicketService.php +++ b/Modules/Helpdesk/Services/TicketService.php @@ -102,9 +102,9 @@ final class TicketService * * @param array $files */ - public function comment(Ticket $ticket, User $user, string $body, bool $internal = false, array $files = [], ?int $parentId = null): Comment + public function comment(Ticket $ticket, User $user, string $body, bool $internal = false, array $files = [], ?int $parentId = null, ?string $subject = null): Comment { - return DB::transaction(function () use ($ticket, $user, $body, $internal, $files, $parentId): Comment { + return DB::transaction(function () use ($ticket, $user, $body, $internal, $files, $parentId, $subject): Comment { $parent = $parentId === null ? null : Comment::query() @@ -117,8 +117,7 @@ final class TicketService $rootUnid = $parent?->root_unid ?: $parentUnid ?: $unid; $plainTextBody = trim(preg_replace('/\s+/', ' ', strip_tags($body)) ?: ''); - /** @var Comment $comment */ - $comment = $ticket->comments()->create([ + $payload = [ 'parent_id' => $parentId, 'user_id' => $user->id, 'body' => $body, @@ -132,7 +131,14 @@ final class TicketService 'sync_sequence' => 1, 'sync_hash' => hash('sha256', $body), 'source_system' => 'wms-helpdesk', - ]); + ]; + + if (Schema::hasColumn('helpdesk_comments', 'subject')) { + $payload['subject'] = trim((string) ($subject ?: ($parentId === null ? 'Response' : 'RE: Response'))); + } + + /** @var Comment $comment */ + $comment = $ticket->comments()->create($payload); $ticket->forceFill(['last_activity_at' => now()])->save(); if (! $internal) { diff --git a/Modules/Helpdesk/Tests/Browser/HelpdeskTicketBrowserTest.php b/Modules/Helpdesk/Tests/Browser/HelpdeskTicketBrowserTest.php index d9010c0a..7a88b923 100644 --- a/Modules/Helpdesk/Tests/Browser/HelpdeskTicketBrowserTest.php +++ b/Modules/Helpdesk/Tests/Browser/HelpdeskTicketBrowserTest.php @@ -79,11 +79,17 @@ final class HelpdeskTicketBrowserTest extends DuskTestCase ->waitForText('Hello Response Browser User', 10) ->assertSee('Inserted canned browser response') ->assertDontSee('

Hello') + ->assertInputValue('subject', 'Browser canned response') ->assertInputValue('commentBody', '

Hello Response Browser User

Inserted canned browser response for '.$ticket->reference.'

') ->press('@post-comment') - ->waitForText('Hello Response Browser User', 10) - ->assertSee('Inserted canned browser response') - ->assertDontSee('

Hello'); + ->waitFor('@helpdesk-ticket-show', 10); + + $this->openHelpdeskConversation($browser); + + $browser->waitForText('Browser canned response', 10) + ->assertSee('Browser canned response') + ->assertDontSee('UNID') + ->assertDontSee('GUID'); $comment = Comment::query() ->where('ticket_id', $ticket->id) @@ -91,17 +97,24 @@ final class HelpdeskTicketBrowserTest extends DuskTestCase ->where('body', 'like', '%Hello Response Browser User%') ->firstOrFail(); + $this->openHelpdeskConversation($browser); + $browser->press('@reply-to-comment-'.$comment->id) ->waitFor('@reply-body-'.$comment->id) ->type('@reply-body-'.$comment->id, 'Nested browser response') ->press('@post-reply-'.$comment->id) - ->waitForText('Nested browser response', 10) - ->assertSee('Nested browser response'); + ->waitFor('@helpdesk-ticket-show', 10); + + $this->openHelpdeskConversation($browser); + + $browser->waitForText('RE: Browser canned response', 10) + ->assertSee('RE: Browser canned response'); }); $this->assertDatabaseHas('helpdesk_comments', [ 'ticket_id' => $ticket->id, 'parent_id' => null, + 'subject' => 'Browser canned response', 'body' => '

Hello Response Browser User

Inserted canned browser response for '.$ticket->reference.'

', ]); @@ -114,10 +127,30 @@ final class HelpdeskTicketBrowserTest extends DuskTestCase $this->assertDatabaseHas('helpdesk_comments', [ 'ticket_id' => $ticket->id, 'parent_id' => $parentId, + 'subject' => 'RE: Browser canned response', 'body' => 'Nested browser response', ]); } + + private function openHelpdeskConversation(Browser $browser): void + { + $browser->script(<< { + card.open = true; +}); +const update = window.updateCommentToggleButtons; +if (typeof update === 'function') { + update(); +} +JS); + $browser->pause(150); + } + private function setHelpdeskEditor(Browser $browser, string $dusk, string $html): void { $encodedDusk = json_encode($dusk, JSON_THROW_ON_ERROR); diff --git a/Modules/Helpdesk/Tests/Feature/TicketConversationTest.php b/Modules/Helpdesk/Tests/Feature/TicketConversationTest.php index 0672e0e1..ed93bd6d 100644 --- a/Modules/Helpdesk/Tests/Feature/TicketConversationTest.php +++ b/Modules/Helpdesk/Tests/Feature/TicketConversationTest.php @@ -74,6 +74,7 @@ final class TicketConversationTest extends TestCase $this->actingAs($owner) ->post(route('helpdesk.tickets.comments.store', $ticket), [ + 'subject' => 'Searchable response subject', 'commentBody' => '

Searchable response

', ]) ->assertRedirect(route('helpdesk.tickets.show', $ticket)); @@ -83,6 +84,7 @@ final class TicketConversationTest extends TestCase self::assertNotEmpty($comment->guid); self::assertNotEmpty($comment->unid); self::assertSame($comment->unid, $comment->root_unid); + self::assertSame('Searchable response subject', $comment->subject); self::assertSame('Searchable response', $comment->body_text); self::assertSame(1, $comment->sync_sequence); } @@ -97,6 +99,7 @@ final class TicketConversationTest extends TestCase $this->actingAs($owner) ->post(route('helpdesk.tickets.comments.store', $ticket), [ 'parent_id' => $parent->id, + 'subject' => 'RE: Parent response', 'commentBody' => '

Child response

', ]) ->assertRedirect(route('helpdesk.tickets.show', $ticket)); @@ -105,6 +108,7 @@ final class TicketConversationTest extends TestCase self::assertSame($parent->unid, $reply->parent_unid); self::assertSame($parent->root_unid, $reply->root_unid); + self::assertSame('RE: Parent response', $reply->subject); } public function test_agent_can_update_status_to_closed_and_priority(): void diff --git a/Modules/Helpdesk/changelog.yaml b/Modules/Helpdesk/changelog.yaml index 3f1f872b..c3786927 100644 --- a/Modules/Helpdesk/changelog.yaml +++ b/Modules/Helpdesk/changelog.yaml @@ -225,3 +225,14 @@ versions: - Updated TicketService comment creation to populate Lotus-style replication identity metadata for document responses and nested responses. - Updated Helpdesk conversation tests to cover POST comment storage, nested reply identity metadata and searchable comment text. - Kept module.json version in sync with changelog.yaml. + +- version: 1.7.0 + date: 2026-06-17 + type: changed + summary: "Polished Lotus Notes-style ticket document and conversation headers, fully collapsed conversation default, subject-led comment cards, and hidden replication identifiers." + changes: + - "Conversation panel now starts fully collapsed and only reveals comment cards when opened." + - "Comment cards now show a professional subject header with author/date metadata and no visible GUID/UNID." + - "Comment subjects are stored for public responses, canned responses, and nested replies while GUID/UNID remain searchable." + - "Ticket details, SLA metrics, requester profile, ticket document, attachments, and conversation headers received consistent professional styling." + - "Browser and feature tests were updated for collapsed conversations, comment subjects, and hidden replication identifiers." diff --git a/Modules/Helpdesk/module.json b/Modules/Helpdesk/module.json index 6d6f4c79..d559d406 100644 --- a/Modules/Helpdesk/module.json +++ b/Modules/Helpdesk/module.json @@ -22,5 +22,5 @@ "Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider" ], "files": [], - "version": "1.3.13" + "version": "1.7.0" }