feat(sidebar) : expose GET /api/sidebar filtered by active modules
This commit is contained in:
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Définition de la sidebar (sections + items). Filtrée par SidebarFilter selon les modules actifs.
|
||||||
|
* Un item porte une clé `module` quand il appartient à un module activable ; sans clé, il est toujours visible.
|
||||||
|
* Les labels sont des clés i18n (sidebar.<domaine>.<item>).
|
||||||
|
*/
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'label' => 'sidebar.general.section',
|
||||||
|
'icon' => 'mdi:view-dashboard-outline',
|
||||||
|
'items' => [
|
||||||
|
['label' => 'sidebar.general.dashboard', 'to' => '/', 'icon' => 'mdi:view-dashboard-outline'],
|
||||||
|
['label' => 'sidebar.general.myTasks', 'to' => '/my-tasks', 'icon' => 'mdi:checkbox-marked-circle-outline'],
|
||||||
|
['label' => 'sidebar.general.projects', 'to' => '/projects', 'icon' => 'mdi:folder-multiple-outline'],
|
||||||
|
['label' => 'sidebar.general.timeTracking', 'to' => '/time-tracking', 'icon' => 'mdi:clock-outline'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => 'sidebar.hr.section',
|
||||||
|
'icon' => 'mdi:calendar-account-outline',
|
||||||
|
'items' => [
|
||||||
|
['label' => 'sidebar.hr.absences', 'to' => '/absences', 'icon' => 'mdi:calendar-remove-outline'],
|
||||||
|
['label' => 'sidebar.hr.teamAbsences', 'to' => '/team-absences', 'icon' => 'mdi:account-group-outline'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Shared\Domain\Sidebar;
|
||||||
|
|
||||||
|
final class SidebarFilter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param list<array{label:string, icon:string, items: list<array{label:string, to:string, icon:string, module?:string}>}> $sections
|
||||||
|
* @param list<string> $activeModuleIds
|
||||||
|
*
|
||||||
|
* @return array{sections: list<array{label:string, icon:string, items: list<array{label:string, to:string, icon:string}>}>, disabledRoutes: list<string>}
|
||||||
|
*/
|
||||||
|
public static function filter(array $sections, array $activeModuleIds): array
|
||||||
|
{
|
||||||
|
$outSections = [];
|
||||||
|
$disabledRoutes = [];
|
||||||
|
|
||||||
|
foreach ($sections as $section) {
|
||||||
|
$items = [];
|
||||||
|
foreach ($section['items'] as $item) {
|
||||||
|
$module = $item['module'] ?? null;
|
||||||
|
if (null !== $module && !in_array($module, $activeModuleIds, true)) {
|
||||||
|
$disabledRoutes[] = $item['to'];
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$items[] = ['label' => $item['label'], 'to' => $item['to'], 'icon' => $item['icon']];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([] !== $items) {
|
||||||
|
$outSections[] = ['label' => $section['label'], 'icon' => $section['icon'], 'items' => $items];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['sections' => $outSections, 'disabledRoutes' => $disabledRoutes];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Shared\Infrastructure\ApiPlatform\Resource;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use App\Shared\Infrastructure\ApiPlatform\State\SidebarProvider;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
|
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(
|
||||||
|
uriTemplate: '/sidebar',
|
||||||
|
normalizationContext: ['groups' => ['sidebar:read']],
|
||||||
|
provider: SidebarProvider::class,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)]
|
||||||
|
final class SidebarResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var list<array{label:string, icon:string, items: list<array{label:string, to:string, icon:string}>}>
|
||||||
|
*/
|
||||||
|
#[Groups(['sidebar:read'])]
|
||||||
|
public array $sections = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
#[Groups(['sidebar:read'])]
|
||||||
|
public array $disabledRoutes = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Shared\Infrastructure\ApiPlatform\State;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\Operation;
|
||||||
|
use ApiPlatform\State\ProviderInterface;
|
||||||
|
use App\Shared\Domain\Module\ModuleRegistry;
|
||||||
|
use App\Shared\Domain\Sidebar\SidebarFilter;
|
||||||
|
use App\Shared\Infrastructure\ApiPlatform\Resource\SidebarResource;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
|
||||||
|
final readonly class SidebarProvider implements ProviderInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
#[Autowire('%kernel.project_dir%')]
|
||||||
|
private string $projectDir,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function provide(Operation $operation, array $uriVariables = [], array $context = []): SidebarResource
|
||||||
|
{
|
||||||
|
/** @var list<class-string> $moduleClasses */
|
||||||
|
$moduleClasses = require $this->projectDir.'/config/modules.php';
|
||||||
|
|
||||||
|
/** @var list<array{label:string, icon:string, items: list<array{label:string, to:string, icon:string, module?:string}>}> $sidebar */
|
||||||
|
$sidebar = require $this->projectDir.'/config/sidebar.php';
|
||||||
|
|
||||||
|
$filtered = SidebarFilter::filter($sidebar, ModuleRegistry::ids($moduleClasses));
|
||||||
|
|
||||||
|
$dto = new SidebarResource();
|
||||||
|
$dto->sections = $filtered['sections'];
|
||||||
|
$dto->disabledRoutes = $filtered['disabledRoutes'];
|
||||||
|
|
||||||
|
return $dto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Functional\Shared;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class SidebarEndpointTest extends WebTestCase
|
||||||
|
{
|
||||||
|
public function testSidebarRequiresAuthentication(): void
|
||||||
|
{
|
||||||
|
$client = self::createClient();
|
||||||
|
$client->request('GET', '/api/sidebar');
|
||||||
|
|
||||||
|
self::assertResponseStatusCodeSame(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSidebarReturnsSectionsForAuthenticatedUser(): void
|
||||||
|
{
|
||||||
|
$client = self::createClient();
|
||||||
|
$container = self::getContainer();
|
||||||
|
$em = $container->get('doctrine.orm.entity_manager');
|
||||||
|
|
||||||
|
$user = $em->getRepository(User::class)->findOneBy(['username' => 'alice']);
|
||||||
|
$client->loginUser($user);
|
||||||
|
|
||||||
|
$client->request('GET', '/api/sidebar');
|
||||||
|
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
$data = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
self::assertArrayHasKey('sections', $data);
|
||||||
|
self::assertArrayHasKey('disabledRoutes', $data);
|
||||||
|
self::assertNotEmpty($data['sections']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Unit\Shared\Sidebar;
|
||||||
|
|
||||||
|
use App\Shared\Domain\Sidebar\SidebarFilter;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class SidebarFilterTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testItemWithoutModuleIsAlwaysVisible(): void
|
||||||
|
{
|
||||||
|
$sections = [
|
||||||
|
['label' => 'sidebar.core.section', 'icon' => 'mdi:home', 'items' => [
|
||||||
|
['label' => 'sidebar.core.dashboard', 'to' => '/', 'icon' => 'mdi:view-dashboard'],
|
||||||
|
]],
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = SidebarFilter::filter($sections, []);
|
||||||
|
|
||||||
|
self::assertCount(1, $result['sections']);
|
||||||
|
self::assertSame('/', $result['sections'][0]['items'][0]['to']);
|
||||||
|
self::assertSame([], $result['disabledRoutes']);
|
||||||
|
self::assertArrayNotHasKey('module', $result['sections'][0]['items'][0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItemWithInactiveModuleIsHiddenAndRouteDisabled(): void
|
||||||
|
{
|
||||||
|
$sections = [
|
||||||
|
['label' => 'sidebar.tt.section', 'icon' => 'mdi:clock', 'items' => [
|
||||||
|
['label' => 'sidebar.tt.timesheet', 'to' => '/time-tracking', 'icon' => 'mdi:clock', 'module' => 'time_tracking'],
|
||||||
|
]],
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = SidebarFilter::filter($sections, []);
|
||||||
|
|
||||||
|
self::assertSame([], $result['sections']);
|
||||||
|
self::assertSame(['/time-tracking'], $result['disabledRoutes']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItemWithActiveModuleIsVisible(): void
|
||||||
|
{
|
||||||
|
$sections = [
|
||||||
|
['label' => 'sidebar.tt.section', 'icon' => 'mdi:clock', 'items' => [
|
||||||
|
['label' => 'sidebar.tt.timesheet', 'to' => '/time-tracking', 'icon' => 'mdi:clock', 'module' => 'time_tracking'],
|
||||||
|
]],
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = SidebarFilter::filter($sections, ['time_tracking']);
|
||||||
|
|
||||||
|
self::assertCount(1, $result['sections']);
|
||||||
|
self::assertSame('/time-tracking', $result['sections'][0]['items'][0]['to']);
|
||||||
|
self::assertSame([], $result['disabledRoutes']);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user