diff --git a/assets/javascripts/controllers/form_ticket_actors_controller.js b/assets/javascripts/controllers/form_ticket_actors_controller.js index 4618e77c..d4fc9c81 100644 --- a/assets/javascripts/controllers/form_ticket_actors_controller.js +++ b/assets/javascripts/controllers/form_ticket_actors_controller.js @@ -29,7 +29,10 @@ export default class extends Controller { return; } - const teamAgentsUids = JSON.parse(selectedTeamOption.dataset.agentsUids); + const agentsIds = selectedTeamOption.dataset.agentsIds; + const agentsUids = selectedTeamOption.dataset.agentsUids; + + const teamAgentsIds = JSON.parse(agentsIds ? agentsIds : agentsUids); for (const option of this.assigneesTarget.options) { if (option.value === '') { @@ -37,7 +40,7 @@ export default class extends Controller { continue; } - if (teamAgentsUids.includes(option.value)) { + if (teamAgentsIds.includes(option.value)) { option.hidden = false; } else { option.hidden = true; diff --git a/src/Controller/BaseController.php b/src/Controller/BaseController.php index 13843d1e..2a37ab30 100644 --- a/src/Controller/BaseController.php +++ b/src/Controller/BaseController.php @@ -41,8 +41,15 @@ protected function isPathRedirectable(string $path): bool } } - protected function createNamedForm(string $name, string $type, mixed $data = null, array $options = []): FormInterface - { + /** + * @param array $options + */ + protected function createNamedForm( + string $name, + string $type, + mixed $data = null, + array $options = [], + ): FormInterface { return $this->container->get('form.factory')->createNamed($name, $type, $data, $options); } } diff --git a/src/Controller/Tickets/ActorsController.php b/src/Controller/Tickets/ActorsController.php index 9cee03f5..40f06c97 100644 --- a/src/Controller/Tickets/ActorsController.php +++ b/src/Controller/Tickets/ActorsController.php @@ -8,6 +8,7 @@ use App\Controller\BaseController; use App\Entity\Ticket; +use App\Form; use App\Repository\TeamRepository; use App\Repository\TicketRepository; use App\Repository\UserRepository; @@ -35,30 +36,18 @@ public function edit( $organization = $ticket->getOrganization(); $this->denyAccessUnlessGranted('orga:update:tickets:actors', $organization); - /** @var \App\Entity\User $user */ + /** @var \App\Entity\User */ $user = $this->getUser(); if (!$ticket->hasActor($user)) { $this->denyAccessUnlessGranted('orga:see:tickets:all', $organization); } - $allUsers = $actorsLister->findByOrganization($organization); - $agents = $actorsLister->findByOrganization($organization, roleType: 'agent'); - $teams = $teamRepository->findByOrganization($organization); - $teamSorter->sort($teams); - - $requester = $ticket->getRequester(); - $team = $ticket->getTeam(); - $assignee = $ticket->getAssignee(); + $form = $this->createNamedForm('ticket_actors', Form\Ticket\ActorsForm::class, $ticket); return $this->render('tickets/actors/edit.html.twig', [ 'ticket' => $ticket, - 'requesterUid' => $requester ? $requester->getUid() : '', - 'teamUid' => $team ? $team->getUid() : '', - 'assigneeUid' => $assignee ? $assignee->getUid() : '', - 'allUsers' => $allUsers, - 'teams' => $teams, - 'agents' => $agents, + 'form' => $form, ]); } @@ -78,89 +67,31 @@ public function update( $organization = $ticket->getOrganization(); $this->denyAccessUnlessGranted('orga:update:tickets:actors', $organization); - /** @var \App\Entity\User $user */ + /** @var \App\Entity\User */ $user = $this->getUser(); if (!$ticket->hasActor($user)) { $this->denyAccessUnlessGranted('orga:see:tickets:all', $organization); } - $initialRequester = $ticket->getRequester(); - $initialTeam = $ticket->getTeam(); - $initialAssignee = $ticket->getAssignee(); - - /** @var string $requesterUid */ - $requesterUid = $request->request->get('requesterUid', $initialRequester ? $initialRequester->getUid() : ''); - - /** @var string $teamUid */ - $teamUid = $request->request->get('teamUid', $initialTeam ? $initialTeam->getUid() : ''); - - /** @var string $assigneeUid */ - $assigneeUid = $request->request->get('assigneeUid', $initialAssignee ? $initialAssignee->getUid() : ''); - - /** @var string $csrfToken */ - $csrfToken = $request->request->get('_csrf_token', ''); - - $allUsers = $actorsLister->findByOrganization($organization); - $agents = $actorsLister->findByOrganization($organization, roleType: 'agent'); - $teams = $teamRepository->findByOrganization($organization); - $teamSorter->sort($teams); - - if (!$this->isCsrfTokenValid('update ticket actors', $csrfToken)) { - return $this->renderBadRequest('tickets/actors/edit.html.twig', [ - 'ticket' => $ticket, - 'requesterUid' => $requesterUid, - 'teamUid' => $teamUid, - 'assigneeUid' => $assigneeUid, - 'allUsers' => $allUsers, - 'teams' => $teams, - 'agents' => $agents, - 'error' => $translator->trans('csrf.invalid', [], 'errors'), - ]); - } - - $requester = ArrayHelper::find($allUsers, function ($user) use ($requesterUid): bool { - return $user->getUid() === $requesterUid; - }); - - $team = null; - if ($teamUid) { - $team = ArrayHelper::find($teams, function ($team) use ($teamUid): bool { - return $team->getUid() === $teamUid; - }); - } - - $assignee = null; - if ($assigneeUid) { - $availableAgents = $team ? $team->getAgents()->toArray() : $agents; - $assignee = ArrayHelper::find($availableAgents, function ($agent) use ($assigneeUid): bool { - return $agent->getUid() === $assigneeUid; - }); - } - $previousAssignee = $ticket->getAssignee(); - $ticket->setRequester($requester); - $ticket->setTeam($team); - $ticket->setAssignee($assignee); + $form = $this->createNamedForm('ticket_actors', Form\Ticket\ActorsForm::class, $ticket); + $form->handleRequest($request); - $errors = $validator->validate($ticket); - if (count($errors) > 0) { + if (!$form->isSubmitted() || !$form->isValid()) { return $this->renderBadRequest('tickets/actors/edit.html.twig', [ 'ticket' => $ticket, - 'requesterUid' => $requesterUid, - 'teamUid' => $teamUid, - 'assigneeUid' => $assigneeUid, - 'allUsers' => $allUsers, - 'teams' => $teams, - 'agents' => $agents, - 'errors' => ConstraintErrorsFormatter::format($errors), + 'form' => $form, ]); } + $ticket = $form->getData(); $ticketRepository->save($ticket, true); - if ($previousAssignee != $assignee) { + $newAssignee = $ticket->getAssignee(); + + if ($previousAssignee != $newAssignee) { $ticketEvent = new TicketEvent($ticket); $eventDispatcher->dispatch($ticketEvent, TicketEvent::ASSIGNED); } diff --git a/src/Entity/Team.php b/src/Entity/Team.php index 211c832b..30cffa4a 100644 --- a/src/Entity/Team.php +++ b/src/Entity/Team.php @@ -99,6 +99,17 @@ public function getAgents(): Collection } /** + * @return int[] + */ + public function getAgentsIds(): array + { + return array_map(function ($agent): ?int { + return $agent->getId(); + }, $this->agents->toArray()); + } + + /** + * @deprecated * @return string[] */ public function getAgentsUids(): array diff --git a/src/Form/Ticket/ActorsForm.php b/src/Form/Ticket/ActorsForm.php new file mode 100644 index 00000000..675b60c6 --- /dev/null +++ b/src/Form/Ticket/ActorsForm.php @@ -0,0 +1,69 @@ +addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event): void { + $form = $event->getForm(); + $ticket = $event->getData(); + + $organization = $ticket->getOrganization(); + + $form->add('requester', Type\ActorType::class, [ + 'organization' => $organization, + 'required' => true, + ]); + + $form->add('team', Type\TeamType::class, [ + 'organization' => $organization, + ]); + + $form->add('assignee', Type\ActorType::class, [ + 'organization' => $organization, + 'roleType' => 'agent', + ]); + }); + + $builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event): void { + $form = $event->getForm(); + $ticket = $event->getData(); + + $team = $ticket->getTeam(); + $assignee = $ticket->getAssignee(); + + if ($team === null || $assignee === null) { + return; + } + + if (!$team->hasAgent($assignee)) { + $error = new FormError('The selected choice is invalid.'); + $form->get('assignee')->addError($error); + } + }); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Entity\Ticket::class, + 'csrf_token_id' => 'ticket actors', + 'csrf_message' => 'csrf.invalid', + ]); + } +} diff --git a/src/Form/Type/ActorType.php b/src/Form/Type/ActorType.php new file mode 100644 index 00000000..5713d46b --- /dev/null +++ b/src/Form/Type/ActorType.php @@ -0,0 +1,64 @@ +setDefaults([ + 'class' => Entity\User::class, + + 'choice_loader' => function (Options $options): ChoiceLoaderInterface { + $organization = $options['organization']; + $roleType = $options['roleType']; + + $vary = [$organization, $roleType]; + + return ChoiceList::lazy( + $this, + function () use ($organization, $roleType): array { + if ($organization) { + return $this->actorsLister->findByOrganization($organization, $roleType); + } else { + return $this->actorsLister->findAll($roleType); + } + }, + $vary, + ); + }, + + 'choice_label' => 'displayName', + 'choice_value' => 'id', + + 'organization' => null, + 'roleType' => 'any', + ]); + + $resolver->setAllowedTypes('organization', [Entity\Organization::class, null]); + $resolver->setAllowedValues('roleType', Service\ActorsLister::VALID_ROLE_TYPES); + } + + public function getParent(): string + { + return Type\EntityType::class; + } +} diff --git a/src/Form/Type/LabelType.php b/src/Form/Type/LabelType.php index adb304f1..5bd0f4f6 100644 --- a/src/Form/Type/LabelType.php +++ b/src/Form/Type/LabelType.php @@ -32,7 +32,6 @@ public function configureOptions(OptionsResolver $resolver): void 'choice_loader' => function (Options $options): ChoiceLoaderInterface { return ChoiceList::lazy( $this, - function () { $labels = $this->labelRepository->findAll(); $this->labelSorter->sort($labels); diff --git a/src/Form/Type/TeamType.php b/src/Form/Type/TeamType.php new file mode 100644 index 00000000..24631f5c --- /dev/null +++ b/src/Form/Type/TeamType.php @@ -0,0 +1,75 @@ +setDefaults([ + 'class' => Entity\Team::class, + + 'choice_loader' => function (Options $options): ChoiceLoaderInterface { + $organization = $options['organization']; + + $vary = [$organization]; + + return ChoiceList::lazy( + $this, + function () use ($organization) { + if ($organization) { + $teams = $this->teamRepository->findByOrganization($organization); + } else { + $teams = $this->teamRepository->findAll(); + } + + $this->teamSorter->sort($teams); + return $teams; + }, + $vary, + ); + }, + + 'choice_label' => 'name', + 'choice_value' => 'id', + + 'choice_attr' => function (Entity\Team $team): array { + $agentsIds = array_map(function (int $id): string { + return (string)$id; + }, $team->getAgentsIds()); + return [ + 'agentsIds' => json_encode($agentsIds), + ]; + }, + + 'organization' => null, + ]); + + $resolver->setAllowedTypes('organization', [Entity\Organization::class, null]); + } + + public function getParent(): string + { + return Type\EntityType::class; + } +} diff --git a/src/Service/ActorsLister.php b/src/Service/ActorsLister.php index ece080dc..9a69cc79 100644 --- a/src/Service/ActorsLister.php +++ b/src/Service/ActorsLister.php @@ -15,6 +15,8 @@ class ActorsLister { + public const VALID_ROLE_TYPES = ['any', 'user', 'agent']; + public function __construct( private OrganizationRepository $orgaRepository, private UserRepository $userRepository, @@ -24,7 +26,7 @@ public function __construct( } /** - * @param 'any'|'user'|'agent' $roleType + * @param value-of $roleType * * @return User[] */ @@ -44,7 +46,7 @@ public function findByOrganization(Organization $organization, string $roleType } /** - * @param 'any'|'user'|'agent' $roleType + * @param value-of $roleType * * @return User[] */ @@ -61,7 +63,7 @@ public function findAll(string $roleType = 'any'): array /** * @param int[] $organizationIds - * @param 'any'|'user'|'agent' $roleType + * @param value-of $roleType * * @return User[] */ diff --git a/templates/tickets/actors/edit.html.twig b/templates/tickets/actors/edit.html.twig index 6f848b1e..ee2a001e 100644 --- a/templates/tickets/actors/edit.html.twig +++ b/templates/tickets/actors/edit.html.twig @@ -9,45 +9,40 @@ {% block title %}{{ 'tickets.actors.edit.title' | trans }}{% endblock %} {% block body %} -
- {% if error %} - {{ include('alerts/_error.html.twig', { message: error }, with_context = false) }} - {% endif %} + {{ form_start(form, { + action: path('update ticket actors', { uid: ticket.uid }), + attr: { + 'class': 'form--standard', + 'data-turbo-preserve-scroll': true, + 'data-controller': 'form-ticket-actors', + }, + }) }} + {{ form_errors(form) }}
-
- {% if teams %} + {% if form.team.vars.choices|length > 0 %}
-
+ {% else %} + {% endif %}
-