diff --git a/app/Filament/Resources/SupportTicketResource.php b/app/Filament/Resources/SupportTicketResource.php index 66aec521b..292ac19a0 100644 --- a/app/Filament/Resources/SupportTicketResource.php +++ b/app/Filament/Resources/SupportTicketResource.php @@ -13,6 +13,7 @@ use Filament\Tables; use Filament\Tables\Table; use Illuminate\Support\HtmlString; +use Illuminate\Support\Js; use Illuminate\Support\Str; class SupportTicketResource extends Resource @@ -78,6 +79,15 @@ public static function infolist(Schema $schema): Schema ->label('Subject'), Infolists\Components\TextEntry::make('message') ->label('Message') + ->hintAction( + Actions\Action::make('copyMessageAsMarkdown') + ->label('Copy as Markdown') + ->icon('heroicon-m-clipboard-document') + ->color('gray') + ->link() + ->visible(fn (SupportTicket $record): bool => filled($record->message)) + ->actionJs(fn (SupportTicket $record): string => self::copyMessageAsMarkdownJs($record)) + ) ->formatStateUsing(fn (?string $state): ?HtmlString => $state === null ? null : new HtmlString(self::renderTicketMessage($state))) @@ -183,18 +193,45 @@ public static function renderTicketMessage(string $message): string return str_replace('
', '
', $html);
}
+ /**
+ * Convert a ticket message to Markdown suitable for pasting elsewhere, turning any
+ * ASCII tables it contains into Markdown tables.
+ */
+ public static function ticketMessageAsMarkdown(string $message): string
+ {
+ return trim(self::convertAsciiTables($message, self::renderAsciiTableAsMarkdown(...)));
+ }
+
+ protected static function copyMessageAsMarkdownJs(SupportTicket $record): string
+ {
+ $markdown = Js::from(self::ticketMessageAsMarkdown($record->message));
+
+ return <<>, hasHeader: bool}|null
+ */
+ protected static function parseAsciiTable(array $lines): ?array
{
$rows = [];
$separatorAfterRow = [];
@@ -248,7 +289,49 @@ protected static function renderAsciiTable(array $lines): ?string
return null;
}
- $hasHeader = count($rows) > 1 && isset($separatorAfterRow[1]);
+ return [
+ 'rows' => $rows,
+ 'hasHeader' => count($rows) > 1 && isset($separatorAfterRow[1]),
+ ];
+ }
+
+ /**
+ * @param list
]*>camera<\/td> ]*>1\.0\.2<\/td><\/tr>/', $html);
}
+ #[Test]
+ public function ticket_message_as_markdown_leaves_plain_markdown_untouched(): void
+ {
+ $message = "**What I was trying to do:**\nDeploy quickly\n\n**What happened instead:**\nIt crashed";
+
+ $this->assertSame($message, SupportTicketResource::ticketMessageAsMarkdown($message));
+ }
+
+ #[Test]
+ public function ticket_message_as_markdown_converts_headerless_ascii_table_to_markdown_table(): void
+ {
+ $message = "Intro line\n+--------------------+---------+\n| Package Version | 3.3.3 |\n"
+ ."| PHP Version (Host) | 8.4.16 |\n+--------------------+---------+\nOutro line";
+
+ $markdown = SupportTicketResource::ticketMessageAsMarkdown($message);
+
+ $this->assertSame(
+ "Intro line\n\n| | |\n| --- | --- |\n| Package Version | 3.3.3 |\n| PHP Version (Host) | 8.4.16 |\n\nOutro line",
+ $markdown
+ );
+ }
+
+ #[Test]
+ public function ticket_message_as_markdown_uses_the_first_row_as_a_header_when_separated(): void
+ {
+ $message = "+----------+---------+\n| Package | Version |\n+----------+---------+\n"
+ ."| camera | 1.0.2 |\n| jump | 2.1.0 |\n+----------+---------+";
+
+ $markdown = SupportTicketResource::ticketMessageAsMarkdown($message);
+
+ $this->assertSame(
+ "| Package | Version |\n| --- | --- |\n| camera | 1.0.2 |\n| jump | 2.1.0 |",
+ $markdown
+ );
+ }
+
+ #[Test]
+ public function ticket_message_as_markdown_pads_ragged_rows_to_the_widest_row(): void
+ {
+ $message = "+---+---+---+\n| a | b | c |\n| d | e |\n+---+---+---+";
+
+ $markdown = SupportTicketResource::ticketMessageAsMarkdown($message);
+
+ $this->assertSame(
+ "| | | |\n| --- | --- | --- |\n| a | b | c |\n| d | e | |",
+ $markdown
+ );
+ }
+
+ #[Test]
+ public function ticket_message_as_markdown_leaves_pipe_lines_that_are_not_tables_alone(): void
+ {
+ $message = "Before\n+------+\nAfter";
+
+ $this->assertSame($message, SupportTicketResource::ticketMessageAsMarkdown($message));
+ }
+
+ #[Test]
+ public function admin_view_page_offers_a_copy_as_markdown_button_for_the_initial_message(): void
+ {
+ $admin = User::factory()->create(['email' => 'admin@test.com']);
+ config(['filament.users' => ['admin@test.com']]);
+
+ $ticket = SupportTicket::factory()->create([
+ 'message' => "**Environment:**\n+---------+-------+\n| Package | 3.3.3 |\n+---------+-------+",
+ ]);
+
+ $html = Livewire::actingAs($admin)
+ ->test(ViewSupportTicket::class, ['record' => $ticket->getRouteKey()])
+ ->assertOk()
+ ->assertSee('Copy as Markdown')
+ ->html();
+
+ $expectedMarkdown = (string) Js::from("**Environment:**\n\n| | |\n| --- | --- |\n| Package | 3.3.3 |");
+
+ $this->assertStringContainsString('window.navigator.clipboard.writeText(', $html);
+ $this->assertStringContainsString($expectedMarkdown, $html);
+ }
+
+ #[Test]
+ public function admin_view_page_hides_the_copy_as_markdown_button_when_there_is_no_message(): void
+ {
+ $admin = User::factory()->create(['email' => 'admin@test.com']);
+ config(['filament.users' => ['admin@test.com']]);
+
+ $ticket = SupportTicket::factory()->create(['message' => '']);
+
+ Livewire::actingAs($admin)
+ ->test(ViewSupportTicket::class, ['record' => $ticket->getRouteKey()])
+ ->assertOk()
+ ->assertDontSee('Copy as Markdown');
+ }
+
#[Test]
public function render_ticket_message_applies_paragraph_spacing(): void
{