new chatgpt module for api intergration
This commit is contained in:
@@ -7,7 +7,16 @@ return [
|
||||
'middleware' => ['web', 'auth'],
|
||||
'disk' => env('HELPDESK_ATTACHMENTS_DISK', 'local'),
|
||||
'max_upload_kb' => (int) env('HELPDESK_MAX_UPLOAD_KB', 10240),
|
||||
'allowed_mimes' => ['md', 'pdf', 'png', 'jpg', 'jpeg', 'webp', 'doc', 'docx', 'xls', 'xlsx', 'txt', 'csv', 'zip'],
|
||||
'allowed_extensions' => [
|
||||
'md', 'pdf', 'png', 'jpg', 'jpeg', 'webp', 'doc', 'docx', 'xls', 'xlsx', 'txt', 'csv', 'log', 'zip',
|
||||
'php', 'phtml', 'phps', 'js', 'jsx', 'ts', 'tsx', 'vue', 'css', 'scss', 'sass', 'less',
|
||||
'html', 'htm', 'xml', 'json', 'yaml', 'yml', 'sql', 'sh', 'bash', 'zsh', 'ps1', 'bat', 'cmd',
|
||||
'py', 'rb', 'go', 'rs', 'java', 'kt', 'kts', 'cs', 'cpp', 'c', 'h', 'hpp', 'swift', 'dart',
|
||||
'rpg', 'rpgle', 'cl', 'clle', 'cbl', 'cob', 'copybook', 'inc', 'env', 'ini', 'conf', 'config',
|
||||
],
|
||||
|
||||
// Backwards-compatible alias for older module code/tests that still read this key.
|
||||
'allowed_mimes' => [],
|
||||
'default_status_slug' => 'new',
|
||||
'closed_status_slug' => 'closed',
|
||||
];
|
||||
|
||||
@@ -22,12 +22,9 @@ final class TicketAttachmentStoreController extends Controller
|
||||
{
|
||||
Gate::authorize('update', $ticket);
|
||||
|
||||
$maxUploadKilobytes = (int) config('helpdesk.max_upload_kb', 10240);
|
||||
$allowedMimes = (array) config('helpdesk.allowed_mimes', ['jpg', 'jpeg', 'png', 'pdf', 'txt', 'log', 'zip']);
|
||||
|
||||
$request->validate([
|
||||
'attachments' => ['required', 'array', 'min:1'],
|
||||
'attachments.*' => ['required', 'file', 'max:' . $maxUploadKilobytes, 'mimes:' . implode(',', $allowedMimes)],
|
||||
'attachments.*' => $this->attachmentFileRules(required: true),
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
@@ -43,4 +40,36 @@ final class TicketAttachmentStoreController extends Controller
|
||||
->route('helpdesk.tickets.show', $ticket)
|
||||
->with('ticket-attachments-added', count($uploadedFiles) . ' supporting file' . (count($uploadedFiles) === 1 ? '' : 's') . ' uploaded.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return Helpdesk attachment validation rules without relying on a newly autoloaded support class.
|
||||
*
|
||||
* @param bool $required Whether at least one uploaded file is required.
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function attachmentFileRules(bool $required = false): array
|
||||
{
|
||||
return array_values(array_filter([
|
||||
$required ? 'required' : null,
|
||||
'file',
|
||||
'max:' . (int) config('helpdesk.max_upload_kb', 10240),
|
||||
'extensions:' . implode(',', $this->allowedAttachmentExtensions()),
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return lower-case Helpdesk attachment extensions.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function allowedAttachmentExtensions(): array
|
||||
{
|
||||
return collect(config('helpdesk.allowed_extensions', config('helpdesk.allowed_mimes', [])))
|
||||
->filter(static fn (mixed $extension): bool => is_string($extension) && trim($extension) !== '')
|
||||
->map(static fn (string $extension): string => strtolower(ltrim(trim($extension), '.')))
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -29,9 +29,6 @@ final class TicketCommentStoreController extends Controller
|
||||
{
|
||||
Gate::authorize('comment', $ticket);
|
||||
|
||||
$maxUploadKilobytes = (int) config('helpdesk.max_upload_kb', 10240);
|
||||
$allowedMimes = (array) config('helpdesk.allowed_mimes', ['jpg', 'jpeg', 'png', 'pdf', 'txt', 'log', 'zip']);
|
||||
|
||||
$validated = $request->validate([
|
||||
'subject' => ['nullable', 'string', 'max:180'],
|
||||
'commentBody' => ['nullable', 'string'],
|
||||
@@ -43,7 +40,7 @@ final class TicketCommentStoreController extends Controller
|
||||
],
|
||||
'canned_response_id' => ['nullable', 'integer', Rule::exists('helpdesk_canned_responses', 'id')],
|
||||
'attachments' => ['nullable', 'array'],
|
||||
'attachments.*' => ['file', 'max:' . $maxUploadKilobytes, 'mimes:' . implode(',', $allowedMimes)],
|
||||
'attachments.*' => $this->attachmentFileRules(),
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
@@ -168,4 +165,36 @@ final class TicketCommentStoreController extends Controller
|
||||
|| Str::lower((string) ($company->slug ?? '')) === 'mpba-limited'
|
||||
|| Str::contains(Str::lower((string) ($company->name ?? '')), 'mpba');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return Helpdesk attachment validation rules without relying on a newly autoloaded support class.
|
||||
*
|
||||
* @param bool $required Whether at least one uploaded file is required.
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function attachmentFileRules(bool $required = false): array
|
||||
{
|
||||
return array_values(array_filter([
|
||||
$required ? 'required' : null,
|
||||
'file',
|
||||
'max:' . (int) config('helpdesk.max_upload_kb', 10240),
|
||||
'extensions:' . implode(',', $this->allowedAttachmentExtensions()),
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return lower-case Helpdesk attachment extensions.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function allowedAttachmentExtensions(): array
|
||||
{
|
||||
return collect(config('helpdesk.allowed_extensions', config('helpdesk.allowed_mimes', [])))
|
||||
->filter(static fn (mixed $extension): bool => is_string($extension) && trim($extension) !== '')
|
||||
->map(static fn (string $extension): string => strtolower(ltrim(trim($extension), '.')))
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,9 +21,6 @@ final class TicketStoreController extends Controller
|
||||
*/
|
||||
public function __invoke(Request $request, TicketService $tickets): RedirectResponse
|
||||
{
|
||||
$maxUploadKilobytes = (int) config('helpdesk.max_upload_kb', 10240);
|
||||
$allowedMimes = (array) config('helpdesk.allowed_mimes', ['jpg', 'jpeg', 'png', 'pdf', 'txt', 'doc', 'docx', 'xls', 'xlsx']);
|
||||
|
||||
$validated = $request->validate([
|
||||
'company_id' => ['nullable', 'integer'],
|
||||
'subject' => ['required', 'string', 'max:200'],
|
||||
@@ -35,7 +32,7 @@ final class TicketStoreController extends Controller
|
||||
'team_id' => ['nullable', 'integer', Rule::exists('helpdesk_teams', 'id')],
|
||||
'due_at' => ['nullable', 'date'],
|
||||
'attachments' => ['nullable', 'array'],
|
||||
'attachments.*' => ['file', 'max:' . $maxUploadKilobytes, 'mimes:' . implode(',', $allowedMimes)],
|
||||
'attachments.*' => $this->attachmentFileRules(),
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
@@ -61,4 +58,36 @@ final class TicketStoreController extends Controller
|
||||
->route('helpdesk.tickets.index')
|
||||
->with('success', 'Ticket ' . $ticket->reference . ' has been created.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return Helpdesk attachment validation rules without relying on a newly autoloaded support class.
|
||||
*
|
||||
* @param bool $required Whether at least one uploaded file is required.
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function attachmentFileRules(bool $required = false): array
|
||||
{
|
||||
return array_values(array_filter([
|
||||
$required ? 'required' : null,
|
||||
'file',
|
||||
'max:' . (int) config('helpdesk.max_upload_kb', 10240),
|
||||
'extensions:' . implode(',', $this->allowedAttachmentExtensions()),
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return lower-case Helpdesk attachment extensions.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function allowedAttachmentExtensions(): array
|
||||
{
|
||||
return collect(config('helpdesk.allowed_extensions', config('helpdesk.allowed_mimes', [])))
|
||||
->filter(static fn (mixed $extension): bool => is_string($extension) && trim($extension) !== '')
|
||||
->map(static fn (string $extension): string => strtolower(ltrim(trim($extension), '.')))
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -74,11 +74,7 @@ final class TicketCreate extends Component
|
||||
'tag_ids' => ['array'],
|
||||
'tag_ids.*' => ['integer', 'exists:helpdesk_tags,id'],
|
||||
'attachments' => ['array'],
|
||||
'attachments.*' => [
|
||||
'file',
|
||||
'max:' . config('helpdesk.max_upload_kb'),
|
||||
'mimes:' . implode(',', config('helpdesk.allowed_mimes')),
|
||||
],
|
||||
'attachments.*' => $this->attachmentFileRules(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -318,4 +314,37 @@ final class TicketCreate extends Component
|
||||
{
|
||||
return Schema::hasTable($table) && Schema::hasColumn($table, 'is_default');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return Helpdesk attachment validation rules without relying on a newly autoloaded support class.
|
||||
*
|
||||
* Laravel validates source-code attachments by extension because browsers often report
|
||||
* PHP, JavaScript, RPG and other source files as text/plain or octet-stream.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function attachmentFileRules(): array
|
||||
{
|
||||
return [
|
||||
'file',
|
||||
'max:' . (int) config('helpdesk.max_upload_kb', 10240),
|
||||
'extensions:' . implode(',', $this->allowedAttachmentExtensions()),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return lower-case Helpdesk attachment extensions.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function allowedAttachmentExtensions(): array
|
||||
{
|
||||
return collect(config('helpdesk.allowed_extensions', config('helpdesk.allowed_mimes', [])))
|
||||
->filter(static fn (mixed $extension): bool => is_string($extension) && trim($extension) !== '')
|
||||
->map(static fn (string $extension): string => strtolower(ltrim(trim($extension), '.')))
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ final class TicketEdit extends Component
|
||||
$this->validate();
|
||||
$this->validate([
|
||||
'attachments' => 'array',
|
||||
'attachments.*' => 'file|max:' . config('helpdesk.max_upload_kb', 10240) . '|mimes:' . implode(',', config('helpdesk.allowed_mimes', ['jpg', 'jpeg', 'png', 'pdf', 'txt', 'log', 'zip'])),
|
||||
'attachments.*' => $this->attachmentFileRules(),
|
||||
'tag_ids.*' => 'integer|exists:helpdesk_tags,id',
|
||||
]);
|
||||
|
||||
@@ -317,4 +317,37 @@ final class TicketEdit extends Component
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return Helpdesk attachment validation rules without relying on a newly autoloaded support class.
|
||||
*
|
||||
* Laravel validates source-code attachments by extension because browsers often report
|
||||
* PHP, JavaScript, RPG and other source files as text/plain or octet-stream.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function attachmentFileRules(): array
|
||||
{
|
||||
return [
|
||||
'file',
|
||||
'max:' . (int) config('helpdesk.max_upload_kb', 10240),
|
||||
'extensions:' . implode(',', $this->allowedAttachmentExtensions()),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return lower-case Helpdesk attachment extensions.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function allowedAttachmentExtensions(): array
|
||||
{
|
||||
return collect(config('helpdesk.allowed_extensions', config('helpdesk.allowed_mimes', [])))
|
||||
->filter(static fn (mixed $extension): bool => is_string($extension) && trim($extension) !== '')
|
||||
->map(static fn (string $extension): string => strtolower(ltrim(trim($extension), '.')))
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ final class TicketShow extends Component
|
||||
|
||||
$this->validate([
|
||||
'commentBody' => ['required', 'string', 'min:2'],
|
||||
'attachments.*' => 'file|max:' . config('helpdesk.max_upload_kb', 10240) . '|mimes:' . implode(',', config('helpdesk.allowed_mimes', ['jpg', 'jpeg', 'png', 'pdf', 'txt', 'log', 'zip'])),
|
||||
'attachments.*' => $this->attachmentFileRules(),
|
||||
]);
|
||||
|
||||
$tickets->comment($this->ticket, Auth::user(), $this->commentBody, $this->internal, $this->attachments);
|
||||
@@ -299,12 +299,9 @@ final class TicketShow extends Component
|
||||
{
|
||||
$this->authorize('comment', $this->ticket);
|
||||
|
||||
$maxUploadKilobytes = (int) config('helpdesk.max_upload_kb', 10240);
|
||||
$allowedMimes = implode(',', config('helpdesk.allowed_mimes', ['jpg', 'jpeg', 'png', 'pdf', 'txt', 'log', 'zip']));
|
||||
|
||||
$this->validate([
|
||||
'supportingFiles' => ['required', 'array', 'min:1'],
|
||||
'supportingFiles.*' => ['file', 'max:' . $maxUploadKilobytes, 'mimes:' . $allowedMimes],
|
||||
'supportingFiles.*' => $this->attachmentFileRules(),
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
@@ -673,4 +670,37 @@ final class TicketShow extends Component
|
||||
return $comment;
|
||||
})->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return Helpdesk attachment validation rules without relying on a newly autoloaded support class.
|
||||
*
|
||||
* Laravel validates source-code attachments by extension because browsers often report
|
||||
* PHP, JavaScript, RPG and other source files as text/plain or octet-stream.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function attachmentFileRules(): array
|
||||
{
|
||||
return [
|
||||
'file',
|
||||
'max:' . (int) config('helpdesk.max_upload_kb', 10240),
|
||||
'extensions:' . implode(',', $this->allowedAttachmentExtensions()),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return lower-case Helpdesk attachment extensions.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function allowedAttachmentExtensions(): array
|
||||
{
|
||||
return collect(config('helpdesk.allowed_extensions', config('helpdesk.allowed_mimes', [])))
|
||||
->filter(static fn (mixed $extension): bool => is_string($extension) && trim($extension) !== '')
|
||||
->map(static fn (string $extension): string => strtolower(ltrim(trim($extension), '.')))
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
@php
|
||||
$helpdeskAttachmentAccept = collect(config('helpdesk.allowed_extensions', config('helpdesk.allowed_mimes', [])))
|
||||
->filter(fn ($extension) => is_string($extension) && trim($extension) !== '')
|
||||
->map(fn ($extension) => '.' . strtolower(ltrim(trim($extension), '.')))
|
||||
->unique()
|
||||
->implode(',');
|
||||
@endphp
|
||||
<div class="hd-helpdesk-surface space-y-6" dusk="helpdesk-ticket-create">
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
@include('helpdesk::partials.page-header', [
|
||||
@@ -7,7 +14,24 @@
|
||||
'Raise a new support request with priority, product/service, project, category, team and full details.',
|
||||
])
|
||||
|
||||
<form wire:submit.prevent="save" x-on:submit.capture="$dispatch('helpdesk-editor-sync', { model: 'body' })" enctype="multipart/form-data" class="helpdesk-ticket-form space-y-6">
|
||||
<form
|
||||
wire:submit.prevent="save"
|
||||
x-data="{ helpdeskAttachmentsUploading: false }"
|
||||
x-on:livewire-upload-start="helpdeskAttachmentsUploading = true"
|
||||
x-on:livewire-upload-finish="helpdeskAttachmentsUploading = false"
|
||||
x-on:livewire-upload-error="helpdeskAttachmentsUploading = false"
|
||||
x-on:livewire-upload-cancel="helpdeskAttachmentsUploading = false"
|
||||
x-on:submit.capture="
|
||||
if (helpdeskAttachmentsUploading) {
|
||||
$event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
$dispatch('helpdesk-editor-sync', { model: 'body' });
|
||||
"
|
||||
enctype="multipart/form-data"
|
||||
class="helpdesk-ticket-form space-y-6"
|
||||
>
|
||||
<div class="overflow-hidden rounded-2xl border border-zinc-700 bg-zinc-900 shadow-sm">
|
||||
<div class="border-b border-zinc-700 bg-zinc-800/80 px-6 py-4">
|
||||
<h2 class="text-lg font-semibold text-zinc-100">Ticket details</h2>
|
||||
@@ -254,7 +278,7 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<input id="attachments" type="file" wire:model="attachments" multiple class="sr-only">
|
||||
<input id="attachments" type="file" wire:model="attachments" multiple accept="{{ $helpdeskAttachmentAccept }}" class="sr-only">
|
||||
|
||||
<div wire:loading.flex wire:target="attachments"
|
||||
class="mt-4 items-center gap-2 rounded-xl border border-blue-500/30 bg-blue-500/10 px-4 py-3 text-sm font-medium text-blue-200">
|
||||
@@ -289,9 +313,10 @@
|
||||
|
||||
<button type="submit"
|
||||
class="inline-flex items-center gap-2 rounded-xl bg-green-600 px-5 py-2 text-sm font-semibold text-white shadow-sm hover:bg-green-700 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
data-dusk="ticket-create-submit" dusk="ticket-create-submit" wire:loading.attr="disabled" wire:target="save" wire:dirty.class="ring-2 ring-amber-400/40">
|
||||
data-dusk="ticket-create-submit" dusk="ticket-create-submit" wire:loading.attr="disabled" wire:target="save,attachments" x-bind:disabled="helpdeskAttachmentsUploading" wire:dirty.class="ring-2 ring-amber-400/40">
|
||||
<i class="fa-regular fa-floppy-disk" aria-hidden="true"></i>
|
||||
<span wire:loading.remove wire:target="save">Create Ticket</span>
|
||||
<span wire:loading.remove wire:target="save,attachments">Create Ticket</span>
|
||||
<span wire:loading wire:target="attachments">Uploading...</span>
|
||||
<span wire:loading wire:target="save">Creating...</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
@php
|
||||
$helpdeskAttachmentAccept = collect(config('helpdesk.allowed_extensions', config('helpdesk.allowed_mimes', [])))
|
||||
->filter(fn ($extension) => is_string($extension) && trim($extension) !== '')
|
||||
->map(fn ($extension) => '.' . strtolower(ltrim(trim($extension), '.')))
|
||||
->unique()
|
||||
->implode(',');
|
||||
@endphp
|
||||
<div class="space-y-6">
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
@include('helpdesk::partials.page-header', [
|
||||
@@ -323,7 +330,7 @@
|
||||
|
||||
<div class="p-4">
|
||||
|
||||
<input id="attachments" type="file" wire:model="attachments" multiple
|
||||
<input id="attachments" type="file" wire:model="attachments" multiple accept="{{ $helpdeskAttachmentAccept }}"
|
||||
class="sr-only">
|
||||
|
||||
<div wire:loading.flex wire:target="attachments"
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
@php
|
||||
$helpdeskAttachmentAccept = collect(config('helpdesk.allowed_extensions', config('helpdesk.allowed_mimes', [])))
|
||||
->filter(fn ($extension) => is_string($extension) && trim($extension) !== '')
|
||||
->map(fn ($extension) => '.' . strtolower(ltrim(trim($extension), '.')))
|
||||
->unique()
|
||||
->implode(',');
|
||||
@endphp
|
||||
@php
|
||||
use Illuminate\Support\Str;
|
||||
use Modules\Helpdesk\Models\TicketTimeLog;
|
||||
@@ -141,7 +148,7 @@
|
||||
class="hd-attachment-action !rounded-lg !px-3 !py-2 !text-xs">
|
||||
<i class="fa-solid fa-paperclip" aria-hidden="true"></i>
|
||||
<span>Choose files</span>
|
||||
<input name="attachments[]" type="file" multiple class="sr-only"
|
||||
<input name="attachments[]" type="file" multiple accept="{{ $helpdeskAttachmentAccept }}" class="sr-only"
|
||||
data-helpdesk-file-input>
|
||||
</label>
|
||||
|
||||
@@ -456,7 +463,7 @@
|
||||
class="inline-flex cursor-pointer items-center justify-center rounded-lg bg-blue-600 px-3 py-2 text-xs font-bold text-white shadow-sm transition hover:bg-blue-700 focus-within:outline-none focus-within:ring-2 focus-within:ring-blue-400/70">
|
||||
<i class="fa-solid fa-paperclip mr-2 text-xs" aria-hidden="true"></i>
|
||||
<span>Choose files</span>
|
||||
<input name="comment_attachments[]" type="file" multiple class="sr-only"
|
||||
<input name="comment_attachments[]" type="file" multiple accept="{{ $helpdeskAttachmentAccept }}" class="sr-only"
|
||||
data-helpdesk-file-input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
@php
|
||||
$helpdeskAttachmentAccept = collect(config('helpdesk.allowed_extensions', config('helpdesk.allowed_mimes', [])))
|
||||
->filter(fn ($extension) => is_string($extension) && trim($extension) !== '')
|
||||
->map(fn ($extension) => '.' . strtolower(ltrim(trim($extension), '.')))
|
||||
->unique()
|
||||
->implode(',');
|
||||
@endphp
|
||||
@props(['comments', 'level' => 0, 'ticket' => null, 'canSeeInternalNotes' => false])
|
||||
|
||||
<div class="space-y-3" data-helpdesk-thread-level="{{ (int) $level }}">
|
||||
@@ -285,7 +292,7 @@
|
||||
<label data-helpdesk-file-button>
|
||||
<i class="fa-solid fa-paperclip" aria-hidden="true"></i>
|
||||
<span>Attach files</span>
|
||||
<input name="attachments[]" type="file" multiple class="sr-only"
|
||||
<input name="attachments[]" type="file" multiple accept="{{ $helpdeskAttachmentAccept }}" class="sr-only"
|
||||
data-helpdesk-file-input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -6,27 +6,40 @@ namespace Modules\Helpdesk\Services;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
use Modules\Helpdesk\Models\Attachment;
|
||||
use Modules\Helpdesk\Models\Comment;
|
||||
use Modules\Helpdesk\Models\Ticket;
|
||||
|
||||
final class AttachmentService
|
||||
{
|
||||
/**
|
||||
* Store multiple Helpdesk attachments against a ticket or comment.
|
||||
*
|
||||
* Livewire create/comment forms pass TemporaryUploadedFile instances, while
|
||||
* controller endpoints pass Symfony/Laravel UploadedFile instances. Both
|
||||
* are supported here so uploads are never silently skipped on ticket create.
|
||||
*
|
||||
* @param array<int|string, mixed> $files
|
||||
*/
|
||||
public function storeMany(Ticket $ticket, array $files, int $userId, ?Comment $comment = null): void
|
||||
{
|
||||
foreach ($files as $file) {
|
||||
if (! $file instanceof UploadedFile || ! $file->isValid()) {
|
||||
if (! $this->isSupportedUpload($file)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$disk = (string) config('helpdesk.disk', 'public');
|
||||
|
||||
$originalName = $file->getClientOriginalName();
|
||||
$mimeType = $file->getClientMimeType();
|
||||
$size = (int) ($file->getSize() ?: 0);
|
||||
|
||||
$originalName = $this->originalName($file);
|
||||
$mimeType = $this->mimeType($file);
|
||||
$size = $this->size($file);
|
||||
$path = $file->store('tickets/' . $ticket->id, $disk);
|
||||
|
||||
if ($size === 0 && $path !== false) {
|
||||
if (! is_string($path) || $path === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($size === 0) {
|
||||
$size = (int) Storage::disk($disk)->size($path);
|
||||
}
|
||||
|
||||
@@ -51,4 +64,40 @@ final class AttachmentService
|
||||
Storage::disk($attachment->disk)->delete($attachment->path);
|
||||
$attachment->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the value is a storeable uploaded file.
|
||||
*/
|
||||
private function isSupportedUpload(mixed $file): bool
|
||||
{
|
||||
if ($file instanceof TemporaryUploadedFile) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $file instanceof UploadedFile && $file->isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the uploaded file's original client filename.
|
||||
*/
|
||||
private function originalName(mixed $file): string
|
||||
{
|
||||
return (string) $file->getClientOriginalName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the uploaded file's reported mime type.
|
||||
*/
|
||||
private function mimeType(mixed $file): string
|
||||
{
|
||||
return (string) ($file->getClientMimeType() ?: $file->getMimeType() ?: 'application/octet-stream');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the uploaded file's byte size.
|
||||
*/
|
||||
private function size(mixed $file): int
|
||||
{
|
||||
return (int) ($file->getSize() ?: 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,41 @@ final class GitAndAttachmentTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function test_existing_ticket_supporting_files_route_accepts_source_code_attachments(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
config()->set('helpdesk.disk', 'local');
|
||||
|
||||
$user = $this->admin();
|
||||
$ticket = $this->ticket(['owner' => $user]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('helpdesk.tickets.attachments.store', $ticket), [
|
||||
'attachments' => [
|
||||
UploadedFile::fake()->create('TicketStoreController.php', 4, 'text/plain'),
|
||||
UploadedFile::fake()->create('ticket-editor.js', 4, 'text/plain'),
|
||||
UploadedFile::fake()->create('warehouse-sync.rpgle', 4, 'text/plain'),
|
||||
],
|
||||
])
|
||||
->assertRedirect(route('helpdesk.tickets.show', $ticket));
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_attachments', [
|
||||
'ticket_id' => $ticket->id,
|
||||
'original_name' => 'TicketStoreController.php',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_attachments', [
|
||||
'ticket_id' => $ticket->id,
|
||||
'original_name' => 'ticket-editor.js',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_attachments', [
|
||||
'ticket_id' => $ticket->id,
|
||||
'original_name' => 'warehouse-sync.rpgle',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_attachment_service_stores_and_deletes_ticket_files(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
|
||||
@@ -236,3 +236,31 @@
|
||||
- Restored native browser spellcheck on the contenteditable editor by allowing typed text to remain stable in the DOM.
|
||||
- Added submit-capture body synchronisation before validation so tickets can be created reliably.
|
||||
- Added regression assertions for editor spellcheck, keypress sync prevention, and submit-time body sync.
|
||||
|
||||
- version: 1.9.53
|
||||
date: 2026-06-24
|
||||
type: feature
|
||||
summary: Allow Helpdesk ticket attachments to include source-code files.
|
||||
changes:
|
||||
- Added central Helpdesk attachment validation based on allowed filename extensions rather than MIME guesses so PHP, JavaScript, RPG and other source files can be uploaded reliably.
|
||||
- Added a broader source-file extension allow-list covering PHP, JS/TS, Vue, CSS, HTML, SQL, shell scripts, Python, Java, C/C++, RPG/RPGLE, CL/CLLE, COBOL and common configuration files.
|
||||
- Updated ticket create, ticket edit, ticket comments and supporting-file upload validation to use the shared attachment rules.
|
||||
- Added source-file accept hints to Helpdesk file inputs.
|
||||
- Added regression coverage for PHP, JavaScript and RPG source-file attachment support.
|
||||
- version: 1.9.54
|
||||
date: 2026-06-24
|
||||
changes:
|
||||
- Removed the new AttachmentValidation support class dependency so source file attachment validation cannot fail when Composer optimized autoload has not been regenerated.
|
||||
- Kept source-code attachment support using Laravel extension validation for PHP, JavaScript, RPG/RPGLE and other source/config files.
|
||||
- Updated ticket create/edit/show and attachment controllers to use existing-class validation helpers.
|
||||
|
||||
- version: 1.9.55
|
||||
date: 2026-06-24
|
||||
type: bugfix
|
||||
summary: Persist attachments when creating Helpdesk tickets.
|
||||
changes:
|
||||
- Prevented ticket create submission while Livewire file uploads are still in progress so tickets cannot be saved before attachments reach the component.
|
||||
- Disabled the create button during both attachment upload and ticket save operations.
|
||||
- Hardened the attachment service to explicitly support Livewire temporary uploads as well as controller uploaded files.
|
||||
- Added regression coverage for create-form upload guarding and Livewire temporary upload support.
|
||||
- Preserved append-only changelog history.
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
|
||||
],
|
||||
"files": [],
|
||||
"version": "1.9.52"
|
||||
"version": "1.9.55"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user