intial ticket fix partial delete fix
This commit is contained in:
@@ -199,6 +199,8 @@ final class SettingsManager extends Component
|
||||
|
||||
public function saveCategory(): void
|
||||
{
|
||||
$this->normalizeNullableFormValues('categoryForm', ['company_id']);
|
||||
|
||||
$data = $this->validate([
|
||||
'categoryForm.company_id' => ['nullable', 'integer'],
|
||||
'categoryForm.name' => ['required', 'string', 'max:120'],
|
||||
@@ -241,6 +243,8 @@ final class SettingsManager extends Component
|
||||
|
||||
public function saveTeam(): void
|
||||
{
|
||||
$this->normalizeNullableFormValues('teamForm', ['company_id']);
|
||||
|
||||
$data = $this->validate([
|
||||
'teamForm.company_id' => ['nullable', 'integer'],
|
||||
'teamForm.name' => ['required', 'string', 'max:120'],
|
||||
@@ -283,6 +287,8 @@ final class SettingsManager extends Component
|
||||
|
||||
public function saveProject(): void
|
||||
{
|
||||
$this->normalizeNullableFormValues('projectForm', ['company_id', 'team_id']);
|
||||
|
||||
$data = $this->validate([
|
||||
'projectForm.company_id' => ['nullable', 'integer'],
|
||||
'projectForm.team_id' => ['nullable', 'integer', 'exists:helpdesk_teams,id'],
|
||||
@@ -326,6 +332,8 @@ final class SettingsManager extends Component
|
||||
|
||||
public function saveBoard(): void
|
||||
{
|
||||
$this->normalizeNullableFormValues('boardForm', ['owner_id']);
|
||||
|
||||
$data = $this->validate([
|
||||
'boardForm.name' => ['required', 'string', 'max:160'],
|
||||
'boardForm.slug' => ['nullable', 'string', 'max:180'],
|
||||
@@ -391,6 +399,8 @@ final class SettingsManager extends Component
|
||||
|
||||
public function saveRepository(): void
|
||||
{
|
||||
$this->normalizeNullableFormValues('repositoryForm', ['company_id', 'project_id', 'webhook_secret']);
|
||||
|
||||
$data = $this->validate([
|
||||
'repositoryForm.company_id' => ['nullable', 'integer'],
|
||||
'repositoryForm.project_id' => ['nullable', 'integer', 'exists:helpdesk_projects,id'],
|
||||
@@ -427,6 +437,8 @@ final class SettingsManager extends Component
|
||||
|
||||
public function saveWorkflowTemplate(): void
|
||||
{
|
||||
$this->normalizeNullableFormValues('workflowForm', ['text_body', 'cc', 'bcc']);
|
||||
|
||||
$data = $this->validate([
|
||||
'workflowForm.name' => ['required', 'string', 'max:160'],
|
||||
'workflowForm.event' => ['required', 'string', 'max:80'],
|
||||
@@ -463,6 +475,8 @@ final class SettingsManager extends Component
|
||||
|
||||
public function saveCannedResponse(): void
|
||||
{
|
||||
$this->normalizeNullableFormValues('cannedForm', ['company_id', 'category_id', 'team_id', 'subject']);
|
||||
|
||||
$data = $this->validate([
|
||||
'cannedForm.company_id' => ['nullable', 'integer'],
|
||||
'cannedForm.category_id' => ['nullable', 'integer', 'exists:helpdesk_categories,id'],
|
||||
@@ -500,6 +514,8 @@ final class SettingsManager extends Component
|
||||
|
||||
public function saveSlaPolicy(): void
|
||||
{
|
||||
$this->normalizeNullableFormValues('slaForm', ['company_id', 'priority_id', 'category_id']);
|
||||
|
||||
$data = $this->validate([
|
||||
'slaForm.company_id' => ['nullable', 'integer'],
|
||||
'slaForm.priority_id' => ['nullable', 'integer', 'exists:helpdesk_priorities,id'],
|
||||
@@ -540,6 +556,8 @@ final class SettingsManager extends Component
|
||||
|
||||
public function saveAutomationRule(): void
|
||||
{
|
||||
$this->normalizeNullableFormValues('automationForm', ['company_id', 'match_sender', 'match_subject', 'match_mailbox', 'match_category_id', 'set_priority_id', 'set_category_id', 'set_team_id', 'set_project_id', 'set_status_id', 'set_agent_id']);
|
||||
|
||||
$data = $this->validate([
|
||||
'automationForm.company_id' => ['nullable', 'integer'],
|
||||
'automationForm.name' => ['required', 'string', 'max:160'],
|
||||
@@ -582,6 +600,8 @@ final class SettingsManager extends Component
|
||||
|
||||
public function saveProduct(): void
|
||||
{
|
||||
$this->normalizeNullableFormValues('productForm', ['company_id', 'version', 'release_channel', 'description']);
|
||||
|
||||
$data = $this->validate([
|
||||
'productForm.company_id' => ['nullable', 'integer'],
|
||||
'productForm.name' => ['required', 'string', 'max:160'],
|
||||
@@ -617,6 +637,8 @@ final class SettingsManager extends Component
|
||||
|
||||
public function saveTag(): void
|
||||
{
|
||||
$this->normalizeNullableFormValues('tagForm', ['company_id']);
|
||||
|
||||
$data = $this->validate([
|
||||
'tagForm.company_id' => ['nullable', 'integer'],
|
||||
'tagForm.name' => ['required', 'string', 'max:80'],
|
||||
@@ -648,6 +670,8 @@ final class SettingsManager extends Component
|
||||
|
||||
public function saveChecklistTemplate(): void
|
||||
{
|
||||
$this->normalizeNullableFormValues('checklistTemplateForm', ['company_id', 'category_id', 'project_id']);
|
||||
|
||||
$data = $this->validate([
|
||||
'checklistTemplateForm.company_id' => ['nullable', 'integer'],
|
||||
'checklistTemplateForm.category_id' => ['nullable', 'integer', 'exists:helpdesk_categories,id'],
|
||||
@@ -694,8 +718,10 @@ final class SettingsManager extends Component
|
||||
return;
|
||||
}
|
||||
|
||||
$this->normalizeNullableScalar('default_helpdesk_company_id');
|
||||
|
||||
$this->validate([
|
||||
'default_helpdesk_company_id' => ['nullable'],
|
||||
'default_helpdesk_company_id' => ['nullable', 'integer', 'exists:companies,id'],
|
||||
]);
|
||||
|
||||
$companyId = filled($this->default_helpdesk_company_id)
|
||||
@@ -796,6 +822,35 @@ final class SettingsManager extends Component
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert browser select empty strings into null before Livewire validation.
|
||||
*
|
||||
* Flux/select controls submit optional values as empty strings. Laravel's
|
||||
* nullable integer and exists rules expect a real null, so normalising here
|
||||
* keeps every settings CRUD form usable from the browser.
|
||||
*
|
||||
* @param array<int, string> $keys
|
||||
*/
|
||||
private function normalizeNullableFormValues(string $formProperty, array $keys): void
|
||||
{
|
||||
foreach ($keys as $key) {
|
||||
if (! array_key_exists($key, $this->{$formProperty})) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (blank($this->{$formProperty}[$key])) {
|
||||
$this->{$formProperty}[$key] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function normalizeNullableScalar(string $property): void
|
||||
{
|
||||
if (blank($this->{$property})) {
|
||||
$this->{$property} = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove is_default before save when the database has not been migrated yet.
|
||||
*
|
||||
|
||||
@@ -9,6 +9,13 @@ use Livewire\Livewire;
|
||||
use Modules\Helpdesk\Livewire\SettingsManager;
|
||||
use Modules\Helpdesk\Models\Category;
|
||||
use Modules\Helpdesk\Models\GitRepository;
|
||||
use Modules\Helpdesk\Models\AutomationRule;
|
||||
use Modules\Helpdesk\Models\CannedResponse;
|
||||
use Modules\Helpdesk\Models\ChecklistTemplate;
|
||||
use Modules\Helpdesk\Models\KanbanBoard;
|
||||
use Modules\Helpdesk\Models\Product;
|
||||
use Modules\Helpdesk\Models\SlaPolicy;
|
||||
use Modules\Helpdesk\Models\Tag;
|
||||
use Modules\Helpdesk\Models\Priority;
|
||||
use Modules\Helpdesk\Models\Project;
|
||||
use Modules\Helpdesk\Models\Status;
|
||||
@@ -95,6 +102,132 @@ final class SettingsCrudTest extends TestCase
|
||||
$this->assertDatabaseHas('helpdesk_workflow_templates', ['name' => 'Ticket Raised', 'event' => 'ticket.created']);
|
||||
}
|
||||
|
||||
|
||||
public function test_settings_manager_creates_every_settings_record_with_browser_empty_select_values(): void
|
||||
{
|
||||
$admin = $this->admin();
|
||||
$status = $this->ticketStatus(['name' => 'Triage', 'slug' => 'triage']);
|
||||
$priority = $this->priority(['name' => 'Standard', 'slug' => 'standard']);
|
||||
$category = $this->category(['name' => 'General', 'slug' => 'general']);
|
||||
$team = $this->team(['name' => 'Support Desk', 'slug' => 'support-desk']);
|
||||
$project = $this->project(['title' => 'Support Portal', 'slug' => 'support-portal']);
|
||||
|
||||
Livewire::actingAs($admin)
|
||||
->test(SettingsManager::class)
|
||||
->set('categoryForm.company_id', '')
|
||||
->set('categoryForm.name', 'Accounts')
|
||||
->set('categoryForm.color', 'blue')
|
||||
->call('saveCategory')
|
||||
->assertHasNoErrors()
|
||||
->set('teamForm.company_id', '')
|
||||
->set('teamForm.name', 'Accounts Team')
|
||||
->set('teamForm.color', 'indigo')
|
||||
->call('saveTeam')
|
||||
->assertHasNoErrors()
|
||||
->set('projectForm.company_id', '')
|
||||
->set('projectForm.team_id', '')
|
||||
->set('projectForm.title', 'Accounts Portal')
|
||||
->set('projectForm.color', 'blue')
|
||||
->call('saveProject')
|
||||
->assertHasNoErrors()
|
||||
->set('boardForm.name', 'Support Board')
|
||||
->set('boardForm.owner_id', '')
|
||||
->set('boardForm.status_ids', [$status->id])
|
||||
->call('saveBoard')
|
||||
->assertHasNoErrors()
|
||||
->set('repositoryForm.company_id', '')
|
||||
->set('repositoryForm.project_id', '')
|
||||
->set('repositoryForm.name', 'Support Repo')
|
||||
->set('repositoryForm.provider', 'github')
|
||||
->set('repositoryForm.remote_url', 'git@example.com:support/repo.git')
|
||||
->set('repositoryForm.default_branch', 'main')
|
||||
->set('repositoryForm.webhook_secret', '')
|
||||
->call('saveRepository')
|
||||
->assertHasNoErrors()
|
||||
->set('workflowForm.name', 'Updated')
|
||||
->set('workflowForm.event', 'ticket.updated')
|
||||
->set('workflowForm.subject', 'Updated {{ ticket.reference }}')
|
||||
->set('workflowForm.html_body', '<p>Updated</p>')
|
||||
->set('workflowForm.text_body', '')
|
||||
->set('workflowForm.cc', '')
|
||||
->set('workflowForm.bcc', '')
|
||||
->set('workflowForm.to_role', 'requester')
|
||||
->call('saveWorkflowTemplate')
|
||||
->assertHasNoErrors()
|
||||
->set('cannedForm.company_id', '')
|
||||
->set('cannedForm.category_id', '')
|
||||
->set('cannedForm.team_id', '')
|
||||
->set('cannedForm.name', 'Greeting')
|
||||
->set('cannedForm.visibility', 'agents')
|
||||
->set('cannedForm.subject', '')
|
||||
->set('cannedForm.body', 'Hello {{ requester.name }}')
|
||||
->call('saveCannedResponse')
|
||||
->assertHasNoErrors()
|
||||
->set('slaForm.company_id', '')
|
||||
->set('slaForm.priority_id', '')
|
||||
->set('slaForm.category_id', '')
|
||||
->set('slaForm.name', 'Standard SLA')
|
||||
->set('slaForm.first_response_minutes', 120)
|
||||
->set('slaForm.resolution_minutes', 1440)
|
||||
->call('saveSlaPolicy')
|
||||
->assertHasNoErrors()
|
||||
->set('automationForm.company_id', '')
|
||||
->set('automationForm.name', 'Route Accounts')
|
||||
->set('automationForm.match_sender', '')
|
||||
->set('automationForm.match_subject', '')
|
||||
->set('automationForm.match_mailbox', '')
|
||||
->set('automationForm.match_category_id', '')
|
||||
->set('automationForm.set_priority_id', $priority->id)
|
||||
->set('automationForm.set_category_id', $category->id)
|
||||
->set('automationForm.set_team_id', $team->id)
|
||||
->set('automationForm.set_project_id', $project->id)
|
||||
->set('automationForm.set_status_id', $status->id)
|
||||
->set('automationForm.set_agent_id', '')
|
||||
->call('saveAutomationRule')
|
||||
->assertHasNoErrors()
|
||||
->set('checklistTemplateForm.company_id', '')
|
||||
->set('checklistTemplateForm.category_id', '')
|
||||
->set('checklistTemplateForm.project_id', '')
|
||||
->set('checklistTemplateForm.name', 'Closure Checks')
|
||||
->set('checklistTemplateForm.items_text', "Confirm requester\nClose ticket")
|
||||
->call('saveChecklistTemplate')
|
||||
->assertHasNoErrors()
|
||||
->set('tagForm.company_id', '')
|
||||
->set('tagForm.name', 'Escalated')
|
||||
->set('tagForm.color', 'red')
|
||||
->call('saveTag')
|
||||
->assertHasNoErrors()
|
||||
->set('productForm.company_id', '')
|
||||
->set('productForm.name', 'WMS Support')
|
||||
->set('productForm.type', 'service')
|
||||
->set('productForm.version', '')
|
||||
->set('productForm.release_channel', '')
|
||||
->set('productForm.description', '')
|
||||
->call('saveProduct')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_categories', ['slug' => 'accounts', 'company_id' => null]);
|
||||
$this->assertDatabaseHas('helpdesk_teams', ['slug' => 'accounts-team', 'company_id' => null]);
|
||||
$this->assertDatabaseHas('helpdesk_projects', ['slug' => 'accounts-portal', 'company_id' => null, 'team_id' => null]);
|
||||
$this->assertDatabaseHas('helpdesk_kanban_boards', ['slug' => 'support-board', 'owner_id' => null]);
|
||||
$this->assertDatabaseHas('helpdesk_git_repositories', ['name' => 'Support Repo', 'project_id' => null, 'webhook_secret' => null]);
|
||||
$this->assertDatabaseHas('helpdesk_workflow_templates', ['name' => 'Updated', 'text_body' => null, 'cc' => null, 'bcc' => null]);
|
||||
$this->assertDatabaseHas('helpdesk_canned_responses', ['slug' => 'greeting', 'category_id' => null, 'team_id' => null]);
|
||||
$this->assertDatabaseHas('helpdesk_sla_policies', ['slug' => 'standard-sla', 'priority_id' => null, 'category_id' => null]);
|
||||
$this->assertDatabaseHas('helpdesk_automation_rules', ['slug' => 'route-accounts', 'set_agent_id' => null]);
|
||||
$this->assertDatabaseHas('helpdesk_checklist_templates', ['slug' => 'closure-checks', 'category_id' => null, 'project_id' => null]);
|
||||
$this->assertDatabaseHas('helpdesk_tags', ['slug' => 'escalated', 'company_id' => null]);
|
||||
$this->assertDatabaseHas('helpdesk_products', ['slug' => 'wms-support', 'company_id' => null]);
|
||||
|
||||
self::assertTrue(KanbanBoard::query()->where('slug', 'support-board')->firstOrFail()->statuses()->whereKey($status->id)->exists());
|
||||
self::assertNotNull(CannedResponse::query()->where('slug', 'greeting')->first());
|
||||
self::assertNotNull(SlaPolicy::query()->where('slug', 'standard-sla')->first());
|
||||
self::assertNotNull(AutomationRule::query()->where('slug', 'route-accounts')->first());
|
||||
self::assertNotNull(ChecklistTemplate::query()->where('slug', 'closure-checks')->first());
|
||||
self::assertNotNull(Tag::query()->where('slug', 'escalated')->first());
|
||||
self::assertNotNull(Product::query()->where('slug', 'wms-support')->first());
|
||||
}
|
||||
|
||||
public function test_settings_manager_can_edit_and_delete_all_new_setup_records(): void
|
||||
{
|
||||
$admin = $this->admin();
|
||||
|
||||
@@ -393,3 +393,12 @@ versions:
|
||||
changed:
|
||||
- Removed the extra visible plus character from Helpdesk New Ticket action labels while keeping the icon-only plus indicator.
|
||||
- Kept module.json version in sync with changelog.yaml.
|
||||
|
||||
- version: 1.9.27
|
||||
date: 2026-06-18
|
||||
changed:
|
||||
- Fixed Helpdesk settings CRUD forms by normalising optional browser select values before Livewire validation.
|
||||
- Hardened optional nullable fields across categories, teams, projects, boards, Git repositories, workflow templates, canned responses, SLA policies, automation rules, checklists, tags, products and default company selection.
|
||||
- Added feature coverage proving every Helpdesk settings area can create records when optional select controls submit empty browser values.
|
||||
- Kept module.json version in sync with changelog.yaml.
|
||||
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
|
||||
],
|
||||
"files": [],
|
||||
"version": "1.9.26"
|
||||
"version": "1.9.27"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user