Skip to content

Commit

Permalink
imp: Allow to select the organization when opening a ticket from the …
Browse files Browse the repository at this point in the history
…global list

Before, clicking on "New ticket" redirected to the "new ticket" form of
the current user's default organization. However, agents often need to
open a ticket in a different organization. In this case, they needed to go
to the organizations' list, find the organization, open it, and click on
"new ticket".

Also, the behavior was inconsistent with the one of the "contracts"
list.

Now, if the user can create a ticket in several organizations, a modal
is opened to allow the user to select the organization.
  • Loading branch information
marien-probesys committed Jan 15, 2025
1 parent fc44fda commit 8c81af0
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 8 deletions.
31 changes: 31 additions & 0 deletions src/Controller/TicketsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Translation\TranslatableMessage;

class TicketsController extends BaseController
{
Expand All @@ -26,6 +27,7 @@ public function index(
Repository\OrganizationRepository $organizationRepository,
SearchEngine\Ticket\Searcher $ticketSearcher,
SearchEngine\Ticket\QuickSearchFilterBuilder $ticketQuickSearchFilterBuilder,
Security\Authorizer $authorizer,
Service\UserService $userService,
): Response {
$page = $request->query->getInt('page', 1);
Expand Down Expand Up @@ -75,6 +77,8 @@ public function index(
$ticketsPagination = Utils\Pagination::empty();
}

$grantedOrganizations = $authorizer->getGrantedOrganizationsToUser($user, permission: 'orga:create:tickets');
$mustSelectOrganization = count($grantedOrganizations) > 1;
$defaultOrganization = $userService->getDefaultOrganization($user);

return $this->render('tickets/index.html.twig', [
Expand All @@ -88,10 +92,37 @@ public function index(
'searchMode' => $searchMode,
'quickSearchForm' => $quickSearchForm,
'advancedSearchForm' => $advancedSearchForm,
'mustSelectOrganization' => $mustSelectOrganization,
'defaultOrganization' => $defaultOrganization,
]);
}

#[Route('/tickets/new', name: 'new ticket')]
public function new(
Request $request,
): Response {
$this->denyAccessUnlessGranted('orga:create:tickets', 'any');

$form = $this->createNamedForm('ticket', Form\Organization\SelectForm::class, options: [
'permission' => 'orga:create:tickets',
'submit_label' => new TranslatableMessage('tickets.new.open_ticket'),
]);

$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
$organization = $form->getData()['organization'];

return $this->redirectToRoute('new organization ticket', [
'uid' => $organization->getUid(),
]);
}

return $this->render('tickets/new.html.twig', [
'form' => $form,
]);
}

#[Route('/tickets/{uid:ticket}', name: 'ticket', methods: ['GET', 'HEAD'])]
public function show(
Entity\Ticket $ticket,
Expand Down
22 changes: 22 additions & 0 deletions src/Security/Authorizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class Authorizer
{
public function __construct(
private Repository\AuthorizationRepository $authorizationRepository,
private Repository\OrganizationRepository $organizationRepository,
private Repository\UserRepository $userRepository,
private Security $security,
private AccessDecisionManagerInterface $accessDecisionManager,
Expand Down Expand Up @@ -123,6 +124,27 @@ public function isGrantedToUser(UserInterface $user, mixed $attribute, mixed $su
return $this->accessDecisionManager->decide($token, [$attribute], $subject);
}

/**
* Return a list of organizations the user has the given permission.
*
* If the permission is "any", it returns all organizations to which the
* user has access.
*
* @return Entity\Organization[]
*/
public function getGrantedOrganizationsToUser(Entity\User $user, string $permission = 'any'): array
{
$organizations = $this->organizationRepository->findAuthorizedOrganizations($user);

if ($permission === 'any') {
return $organizations;
}

return array_filter($organizations, function ($organization) use ($user, $permission): bool {
return $this->isGrantedToUser($user, $permission, $organization);
});
}

/**
* @param Scope $scope
*/
Expand Down
29 changes: 21 additions & 8 deletions templates/tickets/index.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,27 @@
) }}
</div>

{% if defaultOrganization %}
<a
class="button button--primary button--uppercase"
href="{{ path('new organization ticket', { uid: defaultOrganization.uid }) }}"
>
{{ icon('plus') }}
{{ 'tickets.index.new_ticket' | trans }}
</a>
{% if is_granted('orga:create:tickets', 'any') %}
{% if mustSelectOrganization %}
<button
class="button button--primary button--uppercase"
type="button"
data-controller="modal-opener"
data-action="modal-opener#fetch"
data-modal-opener-href-value="{{ path('new ticket') }}"
>
{{ icon('plus') }}
{{ 'tickets.index.new_ticket' | trans }}
</button>
{% else %}
<a
class="button button--primary button--uppercase"
href="{{ path('new organization ticket', { uid: defaultOrganization.uid }) }}"
>
{{ icon('plus') }}
{{ 'tickets.index.new_ticket' | trans }}
</a>
{% endif %}
{% endif %}
</div>

Expand Down
13 changes: 13 additions & 0 deletions templates/tickets/new.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{#
# This file is part of Bileto.
# Copyright 2022-2025 Probesys
# SPDX-License-Identifier: AGPL-3.0-or-later
#}

{% extends 'modal.html.twig' %}

{% block title %}{{ 'tickets.new.title' | trans }}{% endblock %}

{% block body %}
{{ form(form, { action: path('new ticket') }) }}
{% endblock %}
43 changes: 43 additions & 0 deletions tests/Controller/TicketsControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,49 @@ public function testGetIndexRendersCorrectly(): void
$this->assertSelectorTextContains('[data-test="ticket-item"]', "#{$ticket->getId()} My ticket");
}

public function testGetNewRendersCorrectly(): void
{
$client = static::createClient();
$user = Factory\UserFactory::createOne();
$client->loginUser($user->_real());
$this->grantOrga($user->_real(), ['orga:create:tickets']);

$client->request(Request::METHOD_GET, '/tickets/new');

$this->assertResponseIsSuccessful();
$this->assertSelectorTextContains('h1', 'New ticket');
}

public function testGetNewFailsIfAccessIsForbidden(): void
{
$this->expectException(AccessDeniedException::class);

$client = static::createClient();
$user = Factory\UserFactory::createOne();
$client->loginUser($user->_real());

$client->catchExceptions(false);
$client->request(Request::METHOD_GET, '/tickets/new');
}

public function testPostNewRedirectsToTheSelectedOrganization(): void
{
$client = static::createClient();
$user = Factory\UserFactory::createOne();
$client->loginUser($user->_real());
$organization = Factory\OrganizationFactory::createOne();
$this->grantOrga($user->_real(), ['orga:create:tickets']);

$client->request(Request::METHOD_POST, '/tickets/new', [
'ticket' => [
'_token' => $this->generateCsrfToken($client, 'organization select'),
'organization' => $organization->getId(),
],
]);

$this->assertResponseRedirects("/organizations/{$organization->getUid()}/tickets/new", 302);
}

public function testGetShowRendersCorrectlyIfTicketIsCreatedByUser(): void
{
$client = static::createClient();
Expand Down
1 change: 1 addition & 0 deletions translations/messages+intl-icu.en_GB.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@ tickets.list.request_opened_on: 'Request opened on <time datetime="{dateIso}">{d
tickets.list.request_updated_on: 'Request updated on <time datetime="{dateIso}">{date}</time>'
tickets.list.time_spent: '{ count } spent'
tickets.list.time_spent.unaccounted: '{ count } unaccounted'
tickets.new.open_ticket: 'Open a ticket'
tickets.new.submit: 'Create the ticket'
tickets.new.title: 'New ticket'
tickets.observers: Observers
Expand Down
1 change: 1 addition & 0 deletions translations/messages+intl-icu.fr_FR.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@ tickets.list.request_opened_on: 'Demande ouverte le <time datetime="{dateIso}">{
tickets.list.request_updated_on: 'Demande mise à jour le <time datetime="{dateIso}">{date}</time>'
tickets.list.time_spent: '{ count } passée(s)'
tickets.list.time_spent.unaccounted: '{ count } non comptabilisée(s)'
tickets.new.open_ticket: 'Ouvrir un ticket'
tickets.new.submit: 'Créer le ticket'
tickets.new.title: 'Nouveau ticket'
tickets.observers: Observateurs
Expand Down

0 comments on commit 8c81af0

Please sign in to comment.