diff --git a/CHANGELOG.md b/CHANGELOG.md index f1ff8db9..5b5ec214 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ -# Next release +# UNRELEASED * Upgrade the runtime and build toolchain to PHP 8.5 and Node.js 24. * Refresh the CI Docker base image plus Composer and npm dependencies for the PHP 8.5 build. +* Sort the invite roles overview by application display name #343 # 4.1.4 * Update saml2 library. diff --git a/src/OpenConext/Profile/Tests/Value/InviteRoleListTest.php b/src/OpenConext/Profile/Tests/Value/InviteRoleListTest.php new file mode 100644 index 00000000..9194815d --- /dev/null +++ b/src/OpenConext/Profile/Tests/Value/InviteRoleListTest.php @@ -0,0 +1,105 @@ +buildInviteRole('Role C', 'C-application'), + $this->buildInviteRole('Role A', 'A-application'), + $this->buildInviteRole('Role B', 'B-application'), + ]); + + $list->sortByApplicationDisplayName('en'); + + $sorted = array_values(iterator_to_array($list->getIterator())); + $this->assertCount(3, $sorted); + $this->assertEquals('A-application', $sorted[0]->getApplications()[0]->getName('en')); + $this->assertEquals('B-application', $sorted[1]->getApplications()[0]->getName('en')); + $this->assertEquals('C-application', $sorted[2]->getApplications()[0]->getName('en')); + } + + public function test_it_can_order_by_application_display_name_case_insensitively(): void + { + $list = new InviteRoleList([ + $this->buildInviteRole('Role C', 'C-application'), + $this->buildInviteRole('Role A', 'a-application'), + $this->buildInviteRole('Role B', 'B-application'), + ]); + + $list->sortByApplicationDisplayName('en'); + + $sorted = array_values(iterator_to_array($list->getIterator())); + $this->assertCount(3, $sorted); + $this->assertEquals('a-application', $sorted[0]->getApplications()[0]->getName('en')); + $this->assertEquals('B-application', $sorted[1]->getApplications()[0]->getName('en')); + $this->assertEquals('C-application', $sorted[2]->getApplications()[0]->getName('en')); + } + + public function test_roles_without_applications_are_sorted_by_role_name(): void + { + $list = new InviteRoleList([ + $this->buildInviteRole('Role C', 'C-application'), + $this->buildInviteRoleWithoutApplication('Role A'), + $this->buildInviteRole('Role B', 'B-application'), + ]); + + $list->sortByApplicationDisplayName('en'); + + $sorted = array_values(iterator_to_array($list->getIterator())); + $this->assertCount(3, $sorted); + $this->assertEquals('B-application', $sorted[0]->getApplications()[0]->getName('en')); + $this->assertEquals('C-application', $sorted[1]->getApplications()[0]->getName('en')); + $this->assertEquals('Role A', $sorted[2]->getName()); + } + + public function test_it_can_order_nothing(): void + { + $list = new InviteRoleList([]); + $list->sortByApplicationDisplayName('en'); + $this->assertCount(0, $list); + } + + private function buildInviteRole(string $roleName, string $applicationNameEn): InviteRole + { + return new InviteRole($roleName, $roleName, [ + [ + 'landingPage' => '', + 'nameEn' => $applicationNameEn, + 'nameNl' => $applicationNameEn, + 'organisationEn' => '', + 'organisationNl' => '', + 'logo' => '', + ], + ]); + } + + private function buildInviteRoleWithoutApplication(string $roleName): InviteRole + { + return new InviteRole($roleName, $roleName, []); + } +} diff --git a/src/OpenConext/Profile/Value/InviteRoleList.php b/src/OpenConext/Profile/Value/InviteRoleList.php index 5d5704da..702443a1 100644 --- a/src/OpenConext/Profile/Value/InviteRoleList.php +++ b/src/OpenConext/Profile/Value/InviteRoleList.php @@ -61,4 +61,34 @@ public function count(): int { return count($this->roles); } + + /** + * Sorts the roles alphabetically (case-insensitively) by the display name of their + * first application, falling back to the role name for roles without applications. + */ + public function sortByApplicationDisplayName( + string $locale, + ): void { + usort( + $this->roles, + fn(InviteRole $a, InviteRole $b): int => strcasecmp( + $this->getSortNameFor($a, $locale), + $this->getSortNameFor($b, $locale), + ), + ); + } + + private function getSortNameFor( + InviteRole $inviteRole, + string $locale, + ): string { + if ($inviteRole->hasApplications()) { + $applicationName = $inviteRole->getApplications()[0]->getName($locale); + if ($applicationName !== '') { + return $applicationName; + } + } + + return $inviteRole->getName(); + } } diff --git a/src/OpenConext/ProfileBundle/Controller/InviteRolesController.php b/src/OpenConext/ProfileBundle/Controller/InviteRolesController.php index 3c1781a3..32d268e0 100644 --- a/src/OpenConext/ProfileBundle/Controller/InviteRolesController.php +++ b/src/OpenConext/ProfileBundle/Controller/InviteRolesController.php @@ -24,6 +24,7 @@ use OpenConext\Profile\Repository\InviteRepositoryInterface; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Core\User\UserProviderInterface; @@ -44,8 +45,9 @@ public function __construct( methods: ['GET'], schemes: ['https'], )] - public function __invoke(): Response - { + public function __invoke( + Request $request, + ): Response { if (!$this->enabled) { throw $this->createAccessDeniedException(); } @@ -53,6 +55,8 @@ public function __invoke(): Response $user = $this->userProvider->getCurrentUser(); $inviteRoles = $this->inviteRepository->findAllFor($user->getUserIdentifier()); + $inviteRoles->sortByApplicationDisplayName($request->getLocale()); + return $this->render('@OpenConextProfile/InviteRoles/overview.html.twig', ['inviteRoles' => $inviteRoles]); } }