intial ticket fix partial delete fix
This commit is contained in:
@@ -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<Model> $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<TModel> $modelClass
|
||||
* @param int|string|null $id
|
||||
* @param array<string, mixed> $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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,302 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
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;
|
||||
use Modules\Helpdesk\Models\Team;
|
||||
use Modules\Helpdesk\Models\WorkflowTemplate;
|
||||
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class SettingsCrudTest extends TestCase
|
||||
{
|
||||
use BuildsHelpdeskFixtures;
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_settings_manager_creates_updates_and_deletes_statuses(): void
|
||||
{
|
||||
$admin = $this->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', '<p>Hello {{ requester.name }}</p>')
|
||||
->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', '<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();
|
||||
$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' => '<p>Closed</p>',
|
||||
'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]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user