Skip to content

Commit 18a9783

Browse files
simonhampclaude
andauthored
Remove character limit on support ticket environment field (#366)
* Remove 1000 character limit on support ticket environment field Increase the environment field max validation from 1000 to 5000 characters to match the other bug report fields, and add a test. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Only show approved plugins in support ticket plugin selector Filter the plugin/tool dropdown on the support ticket form to only include approved and active official plugins, excluding drafts and pending submissions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Allow inactive approved plugins in support ticket plugin selector Replace the approved() scope (which requires is_active) with a direct status check so disabled plugins still appear for support purposes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add file upload support to support tickets Allow customers and admins to attach files (max 5, 10MB each) when creating tickets and replying. Files are stored on a dedicated support-tickets S3/R2 disk with signed temporary download URLs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update dependencies and rebuild compiled assets Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove hardcoded support-tickets path prefix from file storage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6d61ce2 commit 18a9783

26 files changed

Lines changed: 1321 additions & 519 deletions

File tree

.cursor/rules/laravel-boost.mdc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for
1111
## Foundational Context
1212
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
1313

14-
- php - 8.5.0
14+
- php - 8.4.19
1515
- filament/filament (FILAMENT) - v5
1616
- laravel/cashier (CASHIER) - v15
1717
- laravel/framework (LARAVEL) - v12

.github/copilot-instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for
88
## Foundational Context
99
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
1010

11-
- php - 8.5.0
11+
- php - 8.4.19
1212
- filament/filament (FILAMENT) - v5
1313
- laravel/cashier (CASHIER) - v15
1414
- laravel/framework (LARAVEL) - v12

.junie/guidelines.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for
88
## Foundational Context
99
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
1010

11-
- php - 8.5.0
11+
- php - 8.4.19
1212
- filament/filament (FILAMENT) - v5
1313
- laravel/cashier (CASHIER) - v15
1414
- laravel/framework (LARAVEL) - v12

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for
88
## Foundational Context
99
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
1010

11-
- php - 8.5.0
11+
- php - 8.4.19
1212
- filament/filament (FILAMENT) - v5
1313
- laravel/cashier (CASHIER) - v15
1414
- laravel/framework (LARAVEL) - v12

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for
88
## Foundational Context
99
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
1010

11-
- php - 8.5.0
11+
- php - 8.4.19
1212
- filament/filament (FILAMENT) - v5
1313
- laravel/cashier (CASHIER) - v15
1414
- laravel/framework (LARAVEL) - v12

app/Filament/Resources/SupportTicketResource.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Filament\Schemas\Schema;
1313
use Filament\Tables;
1414
use Filament\Tables\Table;
15+
use Illuminate\Support\HtmlString;
1516

1617
class SupportTicketResource extends Resource
1718
{
@@ -77,6 +78,24 @@ public static function infolist(Schema $schema): Schema
7778
Infolists\Components\TextEntry::make('message')
7879
->label('Message')
7980
->markdown(),
81+
Infolists\Components\TextEntry::make('attachments')
82+
->label('Attachments')
83+
->formatStateUsing(function (SupportTicket $record): HtmlString {
84+
$attachments = $record->attachments;
85+
86+
if (empty($attachments)) {
87+
return new HtmlString('<span style="color: #9ca3af;">None</span>');
88+
}
89+
90+
$links = collect($attachments)->map(function (array $attachment, int $index) use ($record): string {
91+
$url = route('customer.support.tickets.attachment', [$record, $index]);
92+
93+
return '<a href="'.e($url).'" target="_blank" style="color: #2563eb; text-decoration: underline;">'.e($attachment['name']).'</a>';
94+
});
95+
96+
return new HtmlString($links->implode('<br>'));
97+
})
98+
->html(),
8099
])
81100
->collapsible()
82101
->persistCollapsed(),

app/Filament/Resources/SupportTicketResource/Widgets/TicketRepliesWidget.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66
use App\Notifications\SupportTicketReplied;
77
use Filament\Widgets\Widget;
88
use Illuminate\Database\Eloquent\Model;
9+
use Livewire\WithFileUploads;
910

1011
class TicketRepliesWidget extends Widget
1112
{
13+
use WithFileUploads;
14+
1215
protected string $view = 'filament.resources.support-ticket-resource.widgets.ticket-replies';
1316

1417
public ?Model $record = null;
@@ -17,6 +20,8 @@ class TicketRepliesWidget extends Widget
1720

1821
public bool $isNote = false;
1922

23+
public array $replyAttachments = [];
24+
2025
protected int|string|array $columnSpan = 'full';
2126

2227
protected function getListeners(): array
@@ -28,12 +33,32 @@ public function sendReply(): void
2833
{
2934
$this->validate([
3035
'newMessage' => ['required', 'string', 'max:5000'],
36+
'replyAttachments' => ['array', 'max:5'],
37+
'replyAttachments.*' => ['file', 'max:10240'],
3138
]);
3239

40+
$attachments = null;
41+
42+
if (! empty($this->replyAttachments)) {
43+
$attachments = [];
44+
45+
foreach ($this->replyAttachments as $file) {
46+
$path = $file->store("{$this->record->mask}/replies", 'support-tickets');
47+
48+
$attachments[] = [
49+
'name' => $file->getClientOriginalName(),
50+
'path' => $path,
51+
'size' => $file->getSize(),
52+
'mime_type' => $file->getMimeType(),
53+
];
54+
}
55+
}
56+
3357
$reply = $this->record->replies()->create([
3458
'user_id' => auth()->id(),
3559
'message' => $this->newMessage,
3660
'note' => $this->isNote,
61+
'attachments' => $attachments,
3762
]);
3863

3964
if (! $this->isNote && $this->record->user_id !== auth()->id()) {
@@ -42,6 +67,14 @@ public function sendReply(): void
4267

4368
$this->newMessage = '';
4469
$this->isNote = false;
70+
$this->replyAttachments = [];
71+
}
72+
73+
public function removeReplyAttachment(int $index): void
74+
{
75+
$attachments = $this->replyAttachments;
76+
array_splice($attachments, $index, 1);
77+
$this->replyAttachments = $attachments;
4578
}
4679

4780
public function togglePin(int $replyId): void
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
use App\Models\SupportTicket;
6+
use App\Models\SupportTicket\Reply;
7+
use Illuminate\Http\RedirectResponse;
8+
use Illuminate\Support\Facades\Storage;
9+
10+
class SupportTicketAttachmentController extends Controller
11+
{
12+
public function downloadTicketAttachment(SupportTicket $supportTicket, int $index): RedirectResponse
13+
{
14+
$user = auth()->user();
15+
16+
abort_unless($user->id === $supportTicket->user_id || $user->isAdmin(), 403);
17+
18+
$attachments = $supportTicket->attachments ?? [];
19+
20+
abort_unless(isset($attachments[$index]), 404);
21+
22+
$attachment = $attachments[$index];
23+
24+
$url = Storage::disk('support-tickets')->temporaryUrl($attachment['path'], now()->addMinutes(5));
25+
26+
return redirect($url);
27+
}
28+
29+
public function downloadReplyAttachment(SupportTicket $supportTicket, Reply $reply, int $index): RedirectResponse
30+
{
31+
$user = auth()->user();
32+
33+
abort_unless($user->id === $supportTicket->user_id || $user->isAdmin(), 403);
34+
abort_unless($reply->support_ticket_id === $supportTicket->id, 404);
35+
36+
$attachments = $reply->attachments ?? [];
37+
38+
abort_unless(isset($attachments[$index]), 404);
39+
40+
$attachment = $attachments[$index];
41+
42+
$url = Storage::disk('support-tickets')->temporaryUrl($attachment['path'], now()->addMinutes(5));
43+
44+
return redirect($url);
45+
}
46+
}

app/Livewire/Customer/Support/Create.php

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Livewire\Customer\Support;
44

5+
use App\Enums\PluginStatus;
56
use App\Models\Plugin;
67
use App\Models\SupportTicket;
78
use App\Notifications\SupportTicketSubmitted;
@@ -12,11 +13,14 @@
1213
use Livewire\Attributes\Locked;
1314
use Livewire\Attributes\Title;
1415
use Livewire\Component;
16+
use Livewire\WithFileUploads;
1517

1618
#[Layout('components.layouts.dashboard')]
1719
#[Title('Submit a Request')]
1820
class Create extends Component
1921
{
22+
use WithFileUploads;
23+
2024
public function boot(): void
2125
{
2226
abort_unless(auth()->user()->hasUltraAccess(), 403);
@@ -43,6 +47,9 @@ public function boot(): void
4347

4448
public string $environment = '';
4549

50+
/** File uploads */
51+
public array $uploads = [];
52+
4653
/** Step 3: Subject + Message */
4754
public string $subject = '';
4855

@@ -57,6 +64,7 @@ public function updatedSelectedProduct(): void
5764
$this->whatHappened = '';
5865
$this->reproductionSteps = '';
5966
$this->environment = '';
67+
$this->uploads = [];
6068
$this->subject = '';
6169
$this->message = '';
6270
$this->resetValidation();
@@ -139,6 +147,23 @@ public function submit(): void
139147
'metadata' => $metadata ?: null,
140148
]);
141149

150+
if (! empty($this->uploads)) {
151+
$attachments = [];
152+
153+
foreach ($this->uploads as $file) {
154+
$path = $file->store($ticket->mask, 'support-tickets');
155+
156+
$attachments[] = [
157+
'name' => $file->getClientOriginalName(),
158+
'path' => $path,
159+
'size' => $file->getSize(),
160+
'mime_type' => $file->getMimeType(),
161+
];
162+
}
163+
164+
$ticket->update(['attachments' => $attachments]);
165+
}
166+
142167
Notification::route('mail', 'support@nativephp.com')
143168
->notify(new SupportTicketSubmitted($ticket));
144169

@@ -185,7 +210,7 @@ protected function validateStep2(): void
185210
$rules['tryingToDo'] = ['required', 'string', 'max:5000'];
186211
$rules['whatHappened'] = ['required', 'string', 'max:5000'];
187212
$rules['reproductionSteps'] = ['required', 'string', 'max:5000'];
188-
$rules['environment'] = ['required', 'string', 'max:1000'];
213+
$rules['environment'] = ['required', 'string', 'max:5000'];
189214

190215
$messages['tryingToDo.required'] = 'Please describe what you were trying to do.';
191216
$messages['whatHappened.required'] = 'Please describe what happened instead.';
@@ -199,15 +224,30 @@ protected function validateStep2(): void
199224
$messages['issueType.required'] = 'Please select an issue type.';
200225
}
201226

227+
$rules['uploads'] = ['array', 'max:5'];
228+
$rules['uploads.*'] = ['file', 'max:10240'];
229+
230+
$messages['uploads.max'] = 'You may attach up to 5 files.';
231+
$messages['uploads.*.max'] = 'Each file must not exceed 10MB.';
232+
202233
$this->validate($rules, $messages);
203234
}
204235

236+
public function removeUpload(int $index): void
237+
{
238+
$uploads = $this->uploads;
239+
array_splice($uploads, $index, 1);
240+
$this->uploads = $uploads;
241+
}
242+
205243
public function render()
206244
{
207245
$officialPlugins = collect();
208246

209247
if ($this->selectedProduct === 'mobile') {
210-
$officialPlugins = Plugin::where('is_official', true)
248+
$officialPlugins = Plugin::query()
249+
->where('status', PluginStatus::Approved)
250+
->where('is_official', true)
211251
->orderBy('name')
212252
->pluck('name', 'id');
213253
}

app/Livewire/Customer/Support/Show.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,20 @@
1111
use Livewire\Attributes\Layout;
1212
use Livewire\Attributes\Title;
1313
use Livewire\Component;
14+
use Livewire\WithFileUploads;
1415

1516
#[Layout('components.layouts.dashboard')]
1617
#[Title('Support Ticket')]
1718
class Show extends Component
1819
{
20+
use WithFileUploads;
21+
1922
public SupportTicket $supportTicket;
2023

2124
public string $replyMessage = '';
2225

26+
public array $replyAttachments = [];
27+
2328
public function mount(SupportTicket $supportTicket): void
2429
{
2530
abort_unless(auth()->user()->hasUltraAccess(), 403);
@@ -46,20 +51,41 @@ public function reply(): void
4651

4752
$this->validate([
4853
'replyMessage' => ['required', 'string', 'max:5000'],
54+
'replyAttachments' => ['array', 'max:5'],
55+
'replyAttachments.*' => ['file', 'max:10240'],
4956
]);
5057

5158
RateLimiter::hit($key, 60);
5259

60+
$attachments = null;
61+
62+
if (! empty($this->replyAttachments)) {
63+
$attachments = [];
64+
65+
foreach ($this->replyAttachments as $file) {
66+
$path = $file->store("{$this->supportTicket->mask}/replies", 'support-tickets');
67+
68+
$attachments[] = [
69+
'name' => $file->getClientOriginalName(),
70+
'path' => $path,
71+
'size' => $file->getSize(),
72+
'mime_type' => $file->getMimeType(),
73+
];
74+
}
75+
}
76+
5377
$reply = $this->supportTicket->replies()->create([
5478
'user_id' => auth()->id(),
5579
'message' => $this->replyMessage,
5680
'note' => false,
81+
'attachments' => $attachments,
5782
]);
5883

5984
Notification::route('mail', 'support@nativephp.com')
6085
->notify(new SupportTicketUserReplied($this->supportTicket, $reply));
6186

6287
$this->replyMessage = '';
88+
$this->replyAttachments = [];
6389
$this->supportTicket->load(['user', 'replies.user']);
6490
}
6591

@@ -80,6 +106,13 @@ public function closeTicket(): void
80106
$this->supportTicket->load(['user', 'replies.user']);
81107
}
82108

109+
public function removeReplyAttachment(int $index): void
110+
{
111+
$attachments = $this->replyAttachments;
112+
array_splice($attachments, $index, 1);
113+
$this->replyAttachments = $attachments;
114+
}
115+
83116
public function reopenTicket(): void
84117
{
85118
$this->authorize('reopenTicket', $this->supportTicket);

0 commit comments

Comments
 (0)