fix(admin) : recherche insensible a la casse + label permissions dans filtres

SearchFilter partial etait case-sensitive en PostgreSQL : taper "ad" ne
trouvait pas "Administrateur". Passage en strategy `ipartial` (ILIKE) sur
tous les champs texte filtrables :
- Role.label / Role.code
- Site.name / Site.city / Site.postalCode
- User.username

Les filtres exacts sur relations (rbacRoles.code, sites.name,
permissions.code) restent en `exact` — alimentes par des <select> donc
casse maitrisee.

Test de non-regression ajoute : chercher "ad" (minuscule) trouve bien
"Administrateur" (A majuscule) sur /api/roles?label=ad.

UI select permissions du filtre /admin/roles : affiche `perm.label`
("Gerer les roles et permissions") au lieu de `perm.code`
("core.roles.manage"). Value reste sur `code` pour le backend. Tri
par label pour coherence alphabetique avec l'affichage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-20 17:28:01 +02:00
parent 1550f46b23
commit edbe54cb8a
5 changed files with 36 additions and 14 deletions

View File

@@ -55,7 +55,7 @@
:key="perm.id"
:value="perm.code"
>
{{ perm.code }}
{{ perm.label }}
</option>
</select>
</template>
@@ -149,8 +149,11 @@ async function loadPermissions(): Promise<void> {
{ itemsPerPage: 999, orphan: false },
{ toast: false },
)
// Tri par label pour coherence avec l'affichage du <option> : l'user
// lit le label (ex: "Gerer les roles et permissions"), donc l'ordre
// alphabetique doit etre base sur ce qu'il voit, pas sur le code.
allPermissions.value = (data.member ?? []).sort(
(a, b) => a.code.localeCompare(b.code),
(a, b) => a.label.localeCompare(b.label),
)
}

View File

@@ -63,11 +63,13 @@ use Symfony\Component\Validator\Constraints as Assert;
denormalizationContext: ['groups' => ['role:write']],
)]
#[ApiFilter(BooleanFilter::class, properties: ['isSystem'])]
// Filtres /admin/roles : recherche partielle sur label/code + filtre
// exact sur permissions.code (jointure M2M role_permission → permission).
// Filtres /admin/roles : recherche partielle insensible a la casse
// (ILIKE) sur label/code — un admin qui tape "ad" doit trouver
// "Administrateur". Les relations restent en exact (alimentees par un
// <select> cote front, donc casse maitrisee).
#[ApiFilter(SearchFilter::class, properties: [
'label' => 'partial',
'code' => 'partial',
'label' => 'ipartial',
'code' => 'ipartial',
'permissions.code' => 'exact',
])]
#[ORM\Entity(repositoryClass: DoctrineRoleRepository::class)]

View File

@@ -64,13 +64,14 @@ use Symfony\Component\Serializer\Attribute\SerializedName;
],
denormalizationContext: ['groups' => ['user:write']],
)]
// Filtres /admin/users : recherche partielle sur username + filtre bool
// isAdmin + filtres exacts sur les relations (code de role ou nom de site).
// Filtres /admin/users : recherche partielle insensible a la casse
// (ILIKE) sur username + filtre bool isAdmin + filtres exacts sur les
// relations (code de role ou nom de site).
// Les relations sont filtrees par jointure : `rbacRoles.code=admin` declenche
// un INNER JOIN user_role → role. `sites.name=Chatellerault` declenche
// INNER JOIN user_site → site.
#[ApiFilter(SearchFilter::class, properties: [
'username' => 'partial',
'username' => 'ipartial',
'rbacRoles.code' => 'exact',
'sites.name' => 'exact',
])]

View File

@@ -64,12 +64,14 @@ use Symfony\Component\Validator\Constraints as Assert;
normalizationContext: ['groups' => ['site:read']],
denormalizationContext: ['groups' => ['site:write']],
)]
// Filtres cote API pour /admin/sites : recherche partielle (SQL LIKE %x%)
// sur les champs texte saisis dans les headers de la DataTable.
// Filtres cote API pour /admin/sites : recherche partielle insensible a
// la casse (SQL ILIKE %x%) sur les champs texte saisis dans les headers
// de la DataTable. postalCode est purement numerique donc le I/partial
// donne le meme resultat, mais on reste coherent avec name/city.
#[ApiFilter(SearchFilter::class, properties: [
'name' => 'partial',
'city' => 'partial',
'postalCode' => 'partial',
'name' => 'ipartial',
'city' => 'ipartial',
'postalCode' => 'ipartial',
])]
#[ORM\Entity(repositoryClass: DoctrineSiteRepository::class)]
#[ORM\Table(name: 'site')]

View File

@@ -131,6 +131,20 @@ final class AdminFiltersApiTest extends AbstractApiTestCase
}
}
public function testRolesFilterByLabelIsCaseInsensitive(): void
{
// Garde explicite : la strategy est `ipartial` (ILIKE) et pas
// `partial` (LIKE). Chercher "ad" en minuscules DOIT trouver
// "Administrateur" (A majuscule). Si un futur dev retombe en
// strategy `partial` par megarde, ce test cassera.
$client = $this->authenticatedClient('admin', 'admin');
$response = $client->request('GET', '/api/roles?label=ad');
$data = $response->toArray();
$labels = array_column($data['member'], 'label');
self::assertContains('Administrateur', $labels);
}
public function testRolesFilterByCodePartial(): void
{
$client = $this->authenticatedClient('admin', 'admin');