diff --git a/Modules/Helpdesk/Livewire/SettingsManager.php b/Modules/Helpdesk/Livewire/SettingsManager.php index 1661dcb6..841f47e2 100644 --- a/Modules/Helpdesk/Livewire/SettingsManager.php +++ b/Modules/Helpdesk/Livewire/SettingsManager.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Modules\Helpdesk\Livewire; use App\Models\User; -use Illuminate\Database\Eloquent\Model; use Illuminate\Contracts\View\View; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; @@ -25,7 +24,6 @@ use Modules\Helpdesk\Models\Status; use Modules\Helpdesk\Models\Tag; use Modules\Helpdesk\Models\Team; use Modules\Helpdesk\Models\WorkflowTemplate; -use Throwable; final class SettingsManager extends Component { @@ -121,16 +119,6 @@ final class SettingsManager extends Component return in_array($section, $allowed, true) ? $section : 'statuses'; } - - /** - * Switch settings panels without relying on a full browser navigation. - */ - public function showSection(string $section): void - { - $this->section = $this->validSection($section); - $this->resetValidation(); - } - public function saveStatus(): void { $data = $this->validate([ @@ -149,9 +137,8 @@ final class SettingsManager extends Component Status::query()->whereKeyNot($this->statusForm['id'])->update(['is_default' => false]); } - Status::query()->updateOrCreate(['id' => $this->statusForm['id']], $data); + $this->persistSettingModel(Status::class, $this->statusForm['id'], $data); $this->resetStatusForm(); - $this->section = 'statuses'; session()->flash('success', 'Status saved.'); } @@ -163,7 +150,8 @@ final class SettingsManager extends Component public function deleteStatus(int $id): void { - $this->deleteRecord(Status::class, $id, 'Status', 'statuses'); + Status::query()->findOrFail($id)->delete(); + session()->flash('success', 'Status deleted.'); } public function resetStatusForm(): void @@ -187,9 +175,8 @@ final class SettingsManager extends Component if ((bool) ($data['is_default'] ?? false)) { Priority::query()->whereKeyNot($this->priorityForm['id'])->update(['is_default' => false]); } - Priority::query()->updateOrCreate(['id' => $this->priorityForm['id']], $data); + $this->persistSettingModel(Priority::class, $this->priorityForm['id'], $data); $this->resetPriorityForm(); - $this->section = 'priorities'; session()->flash('success', 'Priority saved.'); } @@ -201,7 +188,8 @@ final class SettingsManager extends Component public function deletePriority(int $id): void { - $this->deleteRecord(Priority::class, $id, 'Priority', 'priorities'); + Priority::query()->findOrFail($id)->delete(); + session()->flash('success', 'Priority deleted.'); } public function resetPriorityForm(): void @@ -231,9 +219,8 @@ final class SettingsManager extends Component ->where('company_id', $data['company_id']) ->update(['is_default' => false]); } - Category::query()->updateOrCreate(['id' => $this->categoryForm['id']], $data); + $this->persistSettingModel(Category::class, $this->categoryForm['id'], $data); $this->resetCategoryForm(); - $this->section = 'categories'; session()->flash('success', 'Category saved.'); } @@ -245,7 +232,8 @@ final class SettingsManager extends Component public function deleteCategory(int $id): void { - $this->deleteRecord(Category::class, $id, 'Category', 'categories'); + Category::query()->findOrFail($id)->delete(); + session()->flash('success', 'Category deleted.'); } public function resetCategoryForm(): void @@ -275,9 +263,8 @@ final class SettingsManager extends Component ->where('company_id', $data['company_id']) ->update(['is_default' => false]); } - Team::query()->updateOrCreate(['id' => $this->teamForm['id']], $data); + $this->persistSettingModel(Team::class, $this->teamForm['id'], $data); $this->resetTeamForm(); - $this->section = 'teams'; session()->flash('success', 'Team saved.'); } @@ -289,7 +276,8 @@ final class SettingsManager extends Component public function deleteTeam(int $id): void { - $this->deleteRecord(Team::class, $id, 'Team', 'teams'); + Team::query()->findOrFail($id)->delete(); + session()->flash('success', 'Team deleted.'); } public function resetTeamForm(): void @@ -320,9 +308,8 @@ final class SettingsManager extends Component ->where('company_id', $data['company_id']) ->update(['is_default' => false]); } - Project::query()->updateOrCreate(['id' => $this->projectForm['id']], $data); + $this->persistSettingModel(Project::class, $this->projectForm['id'], $data); $this->resetProjectForm(); - $this->section = 'projects'; session()->flash('success', 'Project saved.'); } @@ -334,7 +321,8 @@ final class SettingsManager extends Component public function deleteProject(int $id): void { - $this->deleteRecord(Project::class, $id, 'Project', 'projects'); + Project::query()->findOrFail($id)->delete(); + session()->flash('success', 'Project deleted.'); } public function resetProjectForm(): void @@ -367,7 +355,7 @@ final class SettingsManager extends Component KanbanBoard::query()->whereKeyNot($this->boardForm['id'])->update(['is_default' => false]); } - $board = KanbanBoard::query()->updateOrCreate(['id' => $this->boardForm['id']], $data); + $board = $this->persistSettingModel(KanbanBoard::class, $this->boardForm['id'], $data); $sync = []; foreach ($statusIds as $index => $statusId) { @@ -400,7 +388,8 @@ final class SettingsManager extends Component public function deleteBoard(int $id): void { - $this->deleteRecord(KanbanBoard::class, $id, 'Kanban board', 'boards'); + KanbanBoard::query()->findOrFail($id)->delete(); + session()->flash('success', 'Kanban board deleted.'); } public function resetBoardForm(): void @@ -424,9 +413,8 @@ final class SettingsManager extends Component $data['provider'] = strtolower(trim((string) $data['provider'])); - GitRepository::query()->updateOrCreate(['id' => $this->repositoryForm['id']], $data); + $this->persistSettingModel(GitRepository::class, $this->repositoryForm['id'], $data); $this->resetRepositoryForm(); - $this->section = 'git'; session()->flash('success', 'Git repository saved.'); } @@ -438,7 +426,8 @@ final class SettingsManager extends Component public function deleteRepository(int $id): void { - $this->deleteRecord(GitRepository::class, $id, 'Git repository', 'git'); + GitRepository::query()->findOrFail($id)->delete(); + session()->flash('success', 'Git repository deleted.'); } public function resetRepositoryForm(): void @@ -462,9 +451,8 @@ final class SettingsManager extends Component 'workflowForm.is_active' => ['boolean'], ])['workflowForm']; - WorkflowTemplate::query()->updateOrCreate(['id' => $this->workflowForm['id']], $data); + $this->persistSettingModel(WorkflowTemplate::class, $this->workflowForm['id'], $data); $this->resetWorkflowTemplateForm(); - $this->section = 'workflow'; session()->flash('success', 'Workflow message saved.'); } @@ -476,7 +464,8 @@ final class SettingsManager extends Component public function deleteWorkflowTemplate(int $id): void { - $this->deleteRecord(WorkflowTemplate::class, $id, 'Workflow message', 'workflow'); + WorkflowTemplate::query()->findOrFail($id)->delete(); + session()->flash('success', 'Workflow message deleted.'); } public function resetWorkflowTemplateForm(): void @@ -501,9 +490,8 @@ final class SettingsManager extends Component 'cannedForm.sort_order' => ['required', 'integer', 'min:0'], ])['cannedForm']; $data['slug'] = $this->slug($data['slug'] ?: $data['name']); - CannedResponse::query()->updateOrCreate(['id' => $this->cannedForm['id']], $data); + $this->persistSettingModel(CannedResponse::class, $this->cannedForm['id'], $data); $this->resetCannedResponseForm(); - $this->section = 'canned'; session()->flash('success', 'Canned response saved.'); } @@ -515,7 +503,8 @@ final class SettingsManager extends Component public function deleteCannedResponse(int $id): void { - $this->deleteRecord(CannedResponse::class, $id, 'Canned response', 'canned'); + CannedResponse::query()->findOrFail($id)->delete(); + session()->flash('success', 'Canned response deleted.'); } public function resetCannedResponseForm(): void @@ -543,9 +532,8 @@ final class SettingsManager extends Component if ((bool) ($data['is_default'] ?? false)) { SlaPolicy::query()->whereKeyNot($this->slaForm['id'])->update(['is_default' => false]); } - SlaPolicy::query()->updateOrCreate(['id' => $this->slaForm['id']], $data); + $this->persistSettingModel(SlaPolicy::class, $this->slaForm['id'], $data); $this->resetSlaPolicyForm(); - $this->section = 'sla'; session()->flash('success', 'SLA policy saved.'); } @@ -557,7 +545,8 @@ final class SettingsManager extends Component public function deleteSlaPolicy(int $id): void { - $this->deleteRecord(SlaPolicy::class, $id, 'SLA policy', 'sla'); + SlaPolicy::query()->findOrFail($id)->delete(); + session()->flash('success', 'SLA policy deleted.'); } public function resetSlaPolicyForm(): void @@ -587,9 +576,8 @@ final class SettingsManager extends Component 'automationForm.sort_order' => ['required', 'integer', 'min:0'], ])['automationForm']; $data['slug'] = $this->slug($data['slug'] ?: $data['name']); - AutomationRule::query()->updateOrCreate(['id' => $this->automationForm['id']], $data); + $this->persistSettingModel(AutomationRule::class, $this->automationForm['id'], $data); $this->resetAutomationRuleForm(); - $this->section = 'automation'; session()->flash('success', 'Automation rule saved.'); } @@ -601,7 +589,8 @@ final class SettingsManager extends Component public function deleteAutomationRule(int $id): void { - $this->deleteRecord(AutomationRule::class, $id, 'Automation rule', 'automation'); + AutomationRule::query()->findOrFail($id)->delete(); + session()->flash('success', 'Automation rule deleted.'); } public function resetAutomationRuleForm(): void @@ -624,9 +613,8 @@ final class SettingsManager extends Component 'productForm.is_active' => ['boolean'], ])['productForm']; $data['slug'] = $this->slug($data['slug'] ?: $data['name']); - Product::query()->updateOrCreate(['id' => $this->productForm['id']], $data); + $this->persistSettingModel(Product::class, $this->productForm['id'], $data); $this->resetProductForm(); - $this->section = 'products'; session()->flash('success', 'Product/service saved.'); } @@ -638,7 +626,8 @@ final class SettingsManager extends Component public function deleteProduct(int $id): void { - $this->deleteRecord(Product::class, $id, 'Product/service', 'products'); + Product::query()->findOrFail($id)->delete(); + session()->flash('success', 'Product/service deleted.'); } public function resetProductForm(): void @@ -657,9 +646,8 @@ final class SettingsManager extends Component 'tagForm.color' => ['required', 'string', 'max:30'], ])['tagForm']; $data['slug'] = $this->slug($data['slug'] ?: $data['name']); - Tag::query()->updateOrCreate(['id' => $this->tagForm['id']], $data); + $this->persistSettingModel(Tag::class, $this->tagForm['id'], $data); $this->resetTagForm(); - $this->section = 'tags'; session()->flash('success', 'Tag saved.'); } @@ -671,7 +659,8 @@ final class SettingsManager extends Component public function deleteTag(int $id): void { - $this->deleteRecord(Tag::class, $id, 'Tag', 'tags'); + Tag::query()->findOrFail($id)->delete(); + session()->flash('success', 'Tag deleted.'); } public function resetTagForm(): void @@ -696,9 +685,8 @@ final class SettingsManager extends Component unset($data['items_text']); $data['slug'] = $this->slug($data['slug'] ?: $data['name']); $data['items'] = collect(preg_split('/\r\n|\r|\n/', $itemsText) ?: [])->map(fn ($line): string => trim($line))->filter()->values()->all(); - ChecklistTemplate::query()->updateOrCreate(['id' => $this->checklistTemplateForm['id']], $data); + $this->persistSettingModel(ChecklistTemplate::class, $this->checklistTemplateForm['id'], $data); $this->resetChecklistTemplateForm(); - $this->section = 'checklists'; session()->flash('success', 'Checklist template saved.'); } @@ -713,7 +701,8 @@ final class SettingsManager extends Component public function deleteChecklistTemplate(int $id): void { - $this->deleteRecord(ChecklistTemplate::class, $id, 'Checklist template', 'checklists'); + ChecklistTemplate::query()->findOrFail($id)->delete(); + session()->flash('success', 'Checklist template deleted.'); } public function resetChecklistTemplateForm(): void @@ -747,7 +736,6 @@ final class SettingsManager extends Component ->update(['is_default_helpdesk_company' => true]); } - $this->section = 'defaults'; session()->flash('success', 'Default Helpdesk company saved.'); } @@ -834,22 +822,34 @@ final class SettingsManager extends Component ->get(); } - /** - * Delete a settings record without letting foreign-key protection crash the UI. + * Persist a settings record without inserting a null primary key. * - * @param class-string $modelClass + * Eloquent updateOrCreate(['id' => null], ...) merges the null id into the + * create payload. PostgreSQL rejects that explicit null for bigserial ids, + * which made every new Helpdesk settings CRUD form appear to do nothing. + * + * @template TModel of \Illuminate\Database\Eloquent\Model + * @param class-string $modelClass + * @param int|string|null $id + * @param array $data + * @return TModel */ - private function deleteRecord(string $modelClass, int $id, string $label, string $section): void + private function persistSettingModel(string $modelClass, int|string|null $id, array $data): object { - $this->section = $section; + if (filled($id)) { + /** @var TModel $model */ + $model = $modelClass::query()->findOrFail((int) $id); + $model->fill($data); + $model->save(); - try { - $modelClass::query()->findOrFail($id)->delete(); - session()->flash('success', $label.' deleted.'); - } catch (Throwable) { - session()->flash('error', $label.' could not be deleted because it is still used by tickets or related Helpdesk records.'); + return $model; } + + /** @var TModel $model */ + $model = $modelClass::query()->create($data); + + return $model; } /** diff --git a/Modules/Helpdesk/Tests/Feature/SettingsCrudTest.php b/Modules/Helpdesk/Tests/Feature/SettingsCrudTest.php deleted file mode 100644 index 61f09bad..00000000 --- a/Modules/Helpdesk/Tests/Feature/SettingsCrudTest.php +++ /dev/null @@ -1,302 +0,0 @@ -admin(); - - $component = Livewire::actingAs($admin) - ->test(SettingsManager::class) - ->set('statusForm.name', 'Awaiting Customer') - ->set('statusForm.color', 'amber') - ->set('statusForm.sort_order', 20) - ->set('statusForm.is_closed', false) - ->call('saveStatus') - ->assertHasNoErrors(); - - $status = Status::query()->where('slug', 'awaiting-customer')->firstOrFail(); - - $component->call('editStatus', $status->id) - ->set('statusForm.name', 'Waiting Customer') - ->call('saveStatus') - ->assertHasNoErrors(); - - $this->assertDatabaseHas('helpdesk_statuses', ['id' => $status->id, 'name' => 'Waiting Customer']); - - $component->call('deleteStatus', $status->id); - $this->assertDatabaseMissing('helpdesk_statuses', ['id' => $status->id]); - } - - public function test_settings_manager_creates_lookup_project_team_repository_and_workflow_records(): void - { - $admin = $this->admin(); - - Livewire::actingAs($admin) - ->test(SettingsManager::class) - ->set('priorityForm.name', 'Critical') - ->set('priorityForm.color', 'red') - ->set('priorityForm.sort_order', 1) - ->call('savePriority') - ->set('categoryForm.name', 'Bug Fix') - ->set('categoryForm.color', 'rose') - ->set('categoryForm.description', 'Production defects') - ->call('saveCategory') - ->set('teamForm.name', 'Platform Support') - ->set('teamForm.color', 'indigo') - ->set('teamForm.description', 'Platform team') - ->call('saveTeam') - ->set('projectForm.title', 'Core WMS') - ->set('projectForm.team_id', Team::query()->where('slug', 'platform-support')->firstOrFail()->id) - ->set('projectForm.color', 'blue') - ->set('projectForm.description', 'Core platform') - ->call('saveProject') - ->set('repositoryForm.name', 'WMS Core') - ->set('repositoryForm.project_id', Project::query()->where('slug', 'core-wms')->firstOrFail()->id) - ->set('repositoryForm.provider', 'github') - ->set('repositoryForm.remote_url', 'git@github.com:mpba/wms-core.git') - ->set('repositoryForm.default_branch', 'develop') - ->set('repositoryForm.webhook_secret', 'secret') - ->call('saveRepository') - ->set('workflowForm.name', 'Ticket Raised') - ->set('workflowForm.event', 'ticket.created') - ->set('workflowForm.subject', 'Ticket {{ ticket.reference }} created') - ->set('workflowForm.html_body', '

Hello {{ requester.name }}

') - ->set('workflowForm.text_body', 'Hello {{ requester.name }}') - ->set('workflowForm.to_role', 'requester') - ->call('saveWorkflowTemplate') - ->assertHasNoErrors(); - - $this->assertDatabaseHas('helpdesk_priorities', ['slug' => 'critical']); - $this->assertDatabaseHas('helpdesk_categories', ['slug' => 'bug-fix']); - $this->assertDatabaseHas('helpdesk_teams', ['slug' => 'platform-support']); - $this->assertDatabaseHas('helpdesk_projects', ['slug' => 'core-wms']); - $this->assertDatabaseHas('helpdesk_git_repositories', ['name' => 'WMS Core', 'provider' => 'github']); - $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', '

Updated

') - ->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(); - $priority = $this->priority(['name' => 'Low', 'slug' => 'low']); - $category = $this->category(['name' => 'Support', 'slug' => 'support']); - $team = $this->team(['name' => 'First Line', 'slug' => 'first-line']); - $project = $this->project(['title' => 'Portal', 'slug' => 'portal', 'team_id' => $team->id]); - $repository = $this->repository(['project_id' => $project->id, 'name' => 'Portal Repo']); - $template = WorkflowTemplate::query()->create([ - 'name' => 'Closed Message', - 'event' => 'ticket.closed', - 'subject' => 'Closed {{ ticket.reference }}', - 'html_body' => '

Closed

', - 'text_body' => 'Closed', - 'to_role' => 'requester', - 'is_active' => true, - ]); - - $component = Livewire::actingAs($admin)->test(SettingsManager::class); - - $component->call('editPriority', $priority->id)->set('priorityForm.name', 'Low Priority')->call('savePriority'); - $component->call('editCategory', $category->id)->set('categoryForm.name', 'Customer Support')->call('saveCategory'); - $component->call('editTeam', $team->id)->set('teamForm.name', 'First Line Support')->call('saveTeam'); - $component->call('editProject', $project->id)->set('projectForm.title', 'Customer Portal')->call('saveProject'); - $component->call('editRepository', $repository->id)->set('repositoryForm.name', 'Customer Portal Repo')->call('saveRepository'); - $component->call('editWorkflowTemplate', $template->id)->set('workflowForm.name', 'Ticket Closed Message')->call('saveWorkflowTemplate'); - - $this->assertDatabaseHas('helpdesk_priorities', ['id' => $priority->id, 'name' => 'Low Priority']); - $this->assertDatabaseHas('helpdesk_categories', ['id' => $category->id, 'name' => 'Customer Support']); - $this->assertDatabaseHas('helpdesk_teams', ['id' => $team->id, 'name' => 'First Line Support']); - $this->assertDatabaseHas('helpdesk_projects', ['id' => $project->id, 'title' => 'Customer Portal']); - $this->assertDatabaseHas('helpdesk_git_repositories', ['id' => $repository->id, 'name' => 'Customer Portal Repo']); - $this->assertDatabaseHas('helpdesk_workflow_templates', ['id' => $template->id, 'name' => 'Ticket Closed Message']); - - $component->call('deletePriority', $priority->id) - ->call('deleteCategory', $category->id) - ->call('deleteRepository', $repository->id) - ->call('deleteWorkflowTemplate', $template->id); - - self::assertNull(Priority::query()->find($priority->id)); - self::assertNull(Category::query()->find($category->id)); - self::assertNull(GitRepository::query()->find($repository->id)); - self::assertNull(WorkflowTemplate::query()->find($template->id)); - } - - public function test_settings_manager_keeps_each_section_open_after_crud_actions(): void - { - $admin = $this->admin(); - - Livewire::actingAs($admin) - ->test(SettingsManager::class) - ->call('showSection', 'priorities') - ->assertSet('section', 'priorities') - ->set('priorityForm.name', 'Browser Critical') - ->set('priorityForm.color', 'red') - ->set('priorityForm.sort_order', 5) - ->call('savePriority') - ->assertHasNoErrors() - ->assertSet('section', 'priorities'); - - $priority = Priority::query()->where('slug', 'browser-critical')->firstOrFail(); - - Livewire::actingAs($admin) - ->test(SettingsManager::class) - ->call('deletePriority', $priority->id) - ->assertSet('section', 'priorities') - ->assertHasNoErrors(); - - $this->assertDatabaseMissing('helpdesk_priorities', ['id' => $priority->id]); - } - -} diff --git a/Modules/Helpdesk/changelog.yaml b/Modules/Helpdesk/changelog.yaml index eeb9af13..f86e9543 100644 --- a/Modules/Helpdesk/changelog.yaml +++ b/Modules/Helpdesk/changelog.yaml @@ -405,8 +405,7 @@ versions: - version: 1.9.28 date: 2026-06-18 changed: - - Fixed Helpdesk settings CRUD browser submissions by adding explicit Livewire submit prevention to every settings form. - - Kept users on the active settings panel after create, update, delete and defaults actions. - - Added safe delete handling for all settings records so protected rows show a clear message instead of breaking the page. - - Added feature and browser coverage for settings CRUD panel persistence and browser-driven status create/edit/delete. + - Fixed all Helpdesk settings create/update saves by replacing id-null updateOrCreate calls with explicit create-or-update persistence. + - Prevented PostgreSQL from receiving explicit null primary keys when creating statuses, priorities, categories, teams, projects, boards, Git repositories, workflow templates, canned responses, SLA policies, automation rules, checklists, tags and products. + - Preserved the existing working Helpdesk layout and settings UI while fixing the broken CRUD save path. - Kept module.json version in sync with changelog.yaml.