فهرست منبع

integrate knpMenu into sonata Admin

Amine Zaghdoudi 10 سال پیش
والد
کامیت
df42796fde

+ 51 - 0
Admin/Pool.php

@@ -13,6 +13,9 @@ namespace Sonata\AdminBundle\Admin;
 
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
+use Knp\Menu\MenuFactory;
+use Knp\Menu\ItemInterface;
+
 class Pool
 {
     protected $container = null;
@@ -330,4 +333,52 @@ class Pool
 
         return $default;
     }
+
+    /**
+     * Create and return a knp MenuItem
+     *
+     * @return ItemInterface
+     */
+    public function getMenu()
+    {
+        $menuFactory = new MenuFactory();
+        $menu = $menuFactory
+            ->createItem('root')
+            ->setExtra('request', $this->container->get('request'))
+        ;
+
+        foreach ($this->getAdminGroups() as $name => $group) {
+            $menu
+                ->addChild($name, array('label' => $group['label']))
+                ->setAttributes(
+                    array(
+                        'icon'  => $group['icon']
+                    )
+                )
+                ->setExtra('roles', $group['roles'])
+            ;
+
+            foreach ($group['items'] as $item) {
+                if (array_key_exists('admin', $item) && $item['admin'] != null) {
+                    $admin             = $this->getInstance($item['admin']);
+                    $label             = $admin->getLabel();
+                    $route             = $admin->generateUrl('list');
+                    $translationDomain = $admin->getTranslationDomain();
+                } else {
+                    $label             = $item['label'];
+                    $route             = $this->container->get('router')->generate($item['route'], $item['route_params']);
+                    $translationDomain = null;
+                    $admin             = null;
+                }
+
+                $menu[$name]
+                    ->addChild($label, array('uri' => $route))
+                    ->setExtra('translationdomain', $translationDomain)
+                    ->setExtra('admin', $admin)
+                ;
+            }
+        }
+
+        return $menu;
+    }
 }

+ 3 - 0
CHANGELOG.md

@@ -1,6 +1,9 @@
 CHANGELOG
 =========
 
+## 2015-02-18
+ * [BC BREAK] Integration of KNPMenu for the admin menu. This integration is resetted when the standard layout ``standard_layout.html.twig`` is overrided. The KNPMenu is available in ``sonata_menu.html.twig`` template.
+
 ### 2015-02-15
  * [BC BREAK] added ``getFieldOption``, ``setFieldOption`` methods to the FilterInterface
  * [BC BREAK] added the ``getFilterFieldDescription`` method to the AdminInterface

+ 1 - 0
DependencyInjection/Compiler/AddDependencyCallsCompilerPass.php

@@ -336,6 +336,7 @@ class AddDependencyCallsCompilerPass implements CompilerPassInterface
             'pager_links'                => 'SonataAdminBundle:Pager:links.html.twig',
             'pager_results'              => 'SonataAdminBundle:Pager:results.html.twig',
             'tab_menu_template'          => 'SonataAdminBundle:Core:tab_menu_template.html.twig',
+            'knp_menu_template'          => 'SonataAdminBundle:Menu:sonata_menu.html.twig',
             'outer_list_rows_mosaic'     => 'SonataAdminBundle:CRUD:list_outer_rows_mosaic.html.twig',
             'outer_list_rows_list'       => 'SonataAdminBundle:CRUD:list_outer_rows_list.html.twig',
             'outer_list_rows_tree'       => 'SonataAdminBundle:CRUD:list_outer_rows_tree.html.twig',

+ 38 - 1
DependencyInjection/Configuration.php

@@ -103,7 +103,43 @@ class Configuration implements ConfigurationInterface
                                     ->scalarNode('label_catalogue')->end()
                                     ->scalarNode('icon')->defaultValue('<i class="fa fa-folder"></i>')->end()
                                     ->arrayNode('items')
-                                        ->prototype('scalar')->end()
+                                        ->beforeNormalization()
+                                            ->ifArray()
+                                            ->then(function($items) {
+                                                foreach ($items as $key => $item) {
+                                                    if (is_array($item)) {
+                                                        if (!array_key_exists('label', $item) || !array_key_exists('route', $item)) {
+                                                            throw new \InvalidArgumentException('Expected either parameters "route" and "label" for array items');
+                                                        }
+
+                                                        if (!array_key_exists('route_params', $item)){
+                                                            $items[$key]['route_params'] = array();
+                                                        }
+
+                                                        $items[$key]['admin'] = '';
+                                                    } else {
+                                                        $items[$key] = array(
+                                                            'admin'        => $item,
+                                                            'label'        => '',
+                                                            'route'        => '',
+                                                            'route_params' => array()
+                                                        );
+                                                    }
+                                                }
+
+                                                return $items;
+                                            })
+                                        ->end()
+                                        ->prototype('array')
+                                            ->children()
+                                                ->scalarNode('admin')->end()
+                                                ->scalarNode('label')->end()
+                                                ->scalarNode('route')->end()
+                                                ->arrayNode('route_params')
+                                                    ->prototype('scalar')->end()
+                                                ->end()
+                                            ->end()
+                                        ->end()
                                     ->end()
                                     ->arrayNode('item_adds')
                                         ->prototype('scalar')->end()
@@ -211,6 +247,7 @@ class Configuration implements ConfigurationInterface
                         ->scalarNode('pager_links')->defaultValue('SonataAdminBundle:Pager:links.html.twig')->cannotBeEmpty()->end()
                         ->scalarNode('pager_results')->defaultValue('SonataAdminBundle:Pager:results.html.twig')->cannotBeEmpty()->end()
                         ->scalarNode('tab_menu_template')->defaultValue('SonataAdminBundle:Core:tab_menu_template.html.twig')->cannotBeEmpty()->end()
+                        ->scalarNode('knp_menu_template')->defaultValue('SonataAdminBundle:Menu:sonata_menu.html.twig')->cannotBeEmpty()->end()
                     ->end()
                 ->end()
 

+ 1 - 0
Resources/doc/index.rst

@@ -25,6 +25,7 @@ Reference Guide
    reference/dashboard
    reference/search
    reference/select2
+   reference/knp_menu
    reference/routing
    reference/action_list
    reference/action_create_edit

+ 62 - 0
Resources/doc/reference/knp_menu.rst

@@ -0,0 +1,62 @@
+KnpMenu
+=======
+
+The admin comes with `KnpMenu <https://github.com/KnpLabs/KnpMenu>`_ integration
+It integrates a menu with the KnpMenu library. This menu can be a SonataAdmin service or a route of a custom controller.
+
+Add a custom controller entry in the menu
+-----------------------------------------
+
+To add a custom controller entry in the admin menu:
+
+Create your controller
+
+.. code-block:: php
+
+    /**
+     * @Route("/blog", name="blog_home")
+     */
+    public function blogAction()
+    {
+        // ...
+    }
+
+    /**
+     * @Route("/blog/article/{articleId}", name="blog_article")
+     */
+    public function ArticleAction($articleId)
+    {
+        // ...
+    }
+
+Add the controller route as an item of the menu
+
+.. code-block:: yaml
+
+    # Default configuration for "SonataAdminBundle"
+    sonata_admin:
+        dashboard:
+            groups:
+                news:
+                    label:                ~
+                    label_catalogue:      ~
+                    items:
+                        - sonata.news.admin.post
+                        - route:        blog_home
+                          label:        Blog
+                        - route:        blog_article
+                          route_params: { articleId: 3 }
+                          label:        Article
+                    ...
+
+Also you can override the template of knp_menu used by sonata. The default one is `SonataAdminBundle:Menu:sonata_menu.html.twig`:
+
+.. code-block:: yaml
+
+    # Default configuration for "SonataAdminBundle"
+    sonata_admin:
+        templates:
+            knp_menu_template:           ApplicationAdminBundle:Menu:custom_knp_menu.html.twig
+        ...
+
+And voilà, now you have a new menu group which contains an entry to sonata_admin_id, to your blog and to a specific article.

+ 69 - 0
Resources/views/Menu/sonata_menu.html.twig

@@ -0,0 +1,69 @@
+{% extends 'knp_menu.html.twig' %}
+
+{% block root %}
+    {%- set listAttributes = item.childrenAttributes|merge({'class': 'sidebar-menu'}) %}
+    {%- set request        = item.getExtra('request') %}
+    {{ block('list') -}}
+{% endblock %}
+
+{% block item %}
+    {%- if item.displayed %}
+        {#- check role of the group #}
+        {%- set display = (item.getExtra('roles') is empty or is_granted('ROLE_SUPER_ADMIN') ) %}
+        {%- for role in item.getExtra('roles') if not display %}
+            {%- set display = is_granted(role) %}
+        {%- endfor %}
+    {%- endif %}
+
+    {%- if item.displayed and display|default %}
+        {%- set active = false %}
+        {%- if item.getExtra('admin') is not empty and item.getExtra('admin').hasroute('list') and item.getExtra('admin').isGranted('LIST') and request.get('_sonata_admin') == item.getExtra('admin').code %}
+            {%- set active = true %}
+        {%- elseif item.route is defined and request.get('_route') == item.route %}
+            {%- set active = true %}
+        {%- else %}
+            {%- for child in item.children if not active %}
+                {%- if child.getExtra('admin') is not empty and child.getExtra('admin').hasroute('list') and child.getExtra('admin').isGranted('LIST') and request.get('_sonata_admin') == child.getExtra('admin').code %}
+                    {%- set active = true %}
+                {%-  elseif child.route is defined and request.get('_route') == child.route %}
+                    {%- set active = true %}
+                {%- endif %}
+            {%- endfor %}
+        {%- endif %}
+
+        {%- if item.level == 1%}
+            {%- do item.setAttribute('class', (item.attribute('class')~' treeview')|trim) %}
+        {%- endif %}
+        {%- if active %}
+            {%- do item.setAttribute('class', (item.attribute('class')~' active')|trim) %}
+            {%- do item.setChildrenAttribute('class', (item.childrenAttribute('class')~' active')|trim) %}
+        {%- endif %}
+
+        {%- do item.setChildrenAttribute('class', (item.childrenAttribute('class')~' treeview-menu')|trim) %}
+        {{ parent() }}
+    {% endif %}
+{% endblock %}
+
+{% block linkElement %}
+    {% spaceless %}
+        {% set translation_domain = item.getExtra('translationdomain', 'messages') %}
+        {% set icon = item.level > 1 ? '<i class="fa fa-angle-double-right"></i>' : '' %}
+        {% set is_link = true %}
+        {{ parent() }}
+    {% endspaceless %}
+{% endblock %}
+
+{% block spanElement %}
+    {% spaceless %}
+
+        <a href="#">
+            {% set translation_domain = item.attribute('label_catalogue') %}
+            {% set icon = item.attribute('icon')|default ? item.attribute('icon') : '' %}
+            {{ icon|default|raw }}
+            {{ parent() }}
+            <i class="fa pull-right fa-angle-left"></i>
+        </a>
+    {% endspaceless %}
+{% endblock %}
+
+{% block label %}{% if is_link is defined and is_link %}{{ icon|default|raw }}{% endif %}{% if options.allow_safe_labels and item.getExtra('safe_label', false) %}{{ item.label|raw }}{% else %}{{ item.label|trans({}, translation_domain|default('messages')) }}{% endif %}{% endblock %}

+ 1 - 41
Resources/views/standard_layout.html.twig

@@ -210,47 +210,7 @@ file that was distributed with this source code.
                                 {% block side_bar_before_nav %} {% endblock %}
                                 {% block side_bar_nav %}
                                     {% if app.security.token and is_granted('ROLE_SONATA_ADMIN') %}
-                                        <ul class="sidebar-menu">
-                                            {% for group in admin_pool.dashboardgroups %}
-                                                {% set display = (group.roles is empty or is_granted('ROLE_SUPER_ADMIN') ) %}
-                                                {% for role in group.roles if not display %}
-                                                    {% set display = is_granted(role) %}
-                                                {% endfor %}
-
-                                                {# Do not display the group label if no item in group is available #}
-                                                {% set item_count = 0 %}
-                                                {% if display %}
-                                                    {% for admin in group.items if item_count == 0 %}
-                                                        {% if admin.hasroute('list') and admin.isGranted('LIST') %}
-                                                            {% set item_count = item_count+1 %}
-                                                        {% endif %}
-                                                    {% endfor %}
-                                                {% endif %}
-
-                                                {% if display and (item_count > 0) %}
-                                                    {% set active = false %}
-                                                    {% for admin in group.items %}
-                                                        {% if admin.hasroute('list') and admin.isGranted('LIST') and app.request.get('_sonata_admin') == admin.code %}
-                                                            {% set active = true %}
-                                                        {% endif %}
-                                                    {% endfor %}
-                                                    <li class="treeview{% if active %} active{% endif %}">
-                                                        <a href="#">
-                                                            {% if group.icon|default() %}{{ group.icon|raw }}{% endif %}
-                                                            <span>{{ group.label|trans({}, group.label_catalogue) }}</span>
-                                                            <i class="fa pull-right fa-angle-left"></i>
-                                                        </a>
-                                                        <ul class="treeview-menu{% if active %} active{% endif %}">
-                                                            {% for admin in group.items %}
-                                                                {% if admin.hasroute('list') and admin.isGranted('LIST') %}
-                                                                    <li{% if app.request.get('_sonata_admin') == admin.code %} class="active"{% endif %}><a href="{{ admin.generateUrl('list')}}"><i class="fa fa-angle-double-right"></i> {{ admin.label|trans({}, admin.translationdomain) }}</a></li>
-                                                                {% endif %}
-                                                            {% endfor %}
-                                                        </ul>
-                                                    </li>
-                                                {% endif %}
-                                            {% endfor %}
-                                        </ul>
+                                        {{ knp_menu_render(admin_pool.menu, {'template' : admin_pool.getTemplate('knp_menu_template')}) }}
                                     {% endif %}
                                 {% endblock side_bar_nav %}
                                 {% block side_bar_after_nav %}

+ 46 - 0
Tests/Admin/PoolTest.php

@@ -254,6 +254,52 @@ class PoolTest extends \PHPUnit_Framework_TestCase
         $this->assertEquals(array(), $this->pool->getOption('nonexistantarray', array()));
     }
 
+    public function testGetMenu()
+    {
+        $containerMock = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
+        $routerMock = $this->getMock('Symfony\Component\Routing\RouterInterface');
+
+        $containerMock->expects($this->any())
+            ->method('get')
+            ->will($this->returnValueMap(
+                array(
+                    array('router', 1, $routerMock),
+                    array('request', 1, null)
+                )
+            ));
+
+        $pool = new Pool($containerMock, 'Sonata Admin', '/path/to/pic.png', array('foo'=>'bar'));
+
+        $adminGroups = array(
+            "bar" => array(
+                "label" => "foo",
+                "icon"  => '<i class="fa fa-edit"></i>',
+                "items" => array(
+                            array(
+                                "admin"        => "",
+                                "label"        => "fooLabel",
+                                "route"        => "FooRoute",
+                                "route_params" => array("foo" => "bar"),
+                            )
+                ),
+                "item_adds" => array(),
+                "roles"     => array()
+
+            )
+        );
+        $pool->setAdminGroups($adminGroups);
+        $menu = $pool->getMenu();
+
+        $this->assertInstanceOf('Knp\Menu\ItemInterface', $menu);
+        $this->assertArrayHasKey('bar', $menu->getChildren());
+
+        foreach ($menu->getChildren() as $key => $child) {
+            $this->assertInstanceOf('Knp\Menu\MenuItem', $child);
+            $this->assertEquals("bar", $child->getName());
+            $this->assertEquals($adminGroups["bar"]["label"], $child->getLabel());
+        }
+    }
+
     /**
      * @return Symfony\Component\DependencyInjection\ContainerInterface - the mock of container interface
      */

+ 36 - 4
Tests/DependencyInjection/Compiler/AddDependencyCallsCompilerPassTest.php

@@ -77,10 +77,33 @@ class AddDependencyCallsCompilerPassTest extends \PHPUnit_Framework_TestCase
         $this->assertArrayHasKey('items', $dashboardGroupsSettings['sonata_group_one']);
         $this->assertArrayHasKey('item_adds', $dashboardGroupsSettings['sonata_group_one']);
         $this->assertArrayHasKey('roles', $dashboardGroupsSettings['sonata_group_one']);
-
         $this->assertEquals('Group One Label', $dashboardGroupsSettings['sonata_group_one']['label']);
         $this->assertEquals('SonataAdminBundle', $dashboardGroupsSettings['sonata_group_one']['label_catalogue']);
-        $this->assertContains('sonata_post_admin', $dashboardGroupsSettings['sonata_group_one']['items']);
+        $this->assertArrayHasKey('admin', $dashboardGroupsSettings['sonata_group_one']['items'][0]);
+        $this->assertArrayHasKey('route', $dashboardGroupsSettings['sonata_group_one']['items'][0]);
+        $this->assertArrayHasKey('label', $dashboardGroupsSettings['sonata_group_one']['items'][0]);
+        $this->assertArrayHasKey('route_params', $dashboardGroupsSettings['sonata_group_one']['items'][0]);
+        $this->assertContains('sonata_post_admin', $dashboardGroupsSettings['sonata_group_one']['items'][0]);
+        $this->assertArrayHasKey('admin', $dashboardGroupsSettings['sonata_group_one']['items'][1]);
+        $this->assertArrayHasKey('route', $dashboardGroupsSettings['sonata_group_one']['items'][1]);
+        $this->assertArrayHasKey('label', $dashboardGroupsSettings['sonata_group_one']['items'][1]);
+        $this->assertArrayHasKey('route_params', $dashboardGroupsSettings['sonata_group_one']['items'][1]);
+        $this->assertContains('blog_name', $dashboardGroupsSettings['sonata_group_one']['items'][1]);
+        $this->assertContains('Blog', $dashboardGroupsSettings['sonata_group_one']['items'][1]);
+        $this->assertEquals('', $dashboardGroupsSettings['sonata_group_one']['items'][1]['admin']);
+        $this->assertEquals('blog_name', $dashboardGroupsSettings['sonata_group_one']['items'][1]['route']);
+        $this->assertEquals('Blog', $dashboardGroupsSettings['sonata_group_one']['items'][1]['label']);
+        $this->assertEquals(array(), $dashboardGroupsSettings['sonata_group_one']['items'][1]['route_params']);
+        $this->assertArrayHasKey('admin', $dashboardGroupsSettings['sonata_group_one']['items'][2]);
+        $this->assertArrayHasKey('route', $dashboardGroupsSettings['sonata_group_one']['items'][2]);
+        $this->assertArrayHasKey('label', $dashboardGroupsSettings['sonata_group_one']['items'][2]);
+        $this->assertArrayHasKey('route_params', $dashboardGroupsSettings['sonata_group_one']['items'][2]);
+        $this->assertContains('blog_article', $dashboardGroupsSettings['sonata_group_one']['items'][2]);
+        $this->assertContains('Article', $dashboardGroupsSettings['sonata_group_one']['items'][2]);
+        $this->assertEquals('', $dashboardGroupsSettings['sonata_group_one']['items'][2]['admin']);
+        $this->assertEquals('blog_article', $dashboardGroupsSettings['sonata_group_one']['items'][2]['route']);
+        $this->assertEquals('Article', $dashboardGroupsSettings['sonata_group_one']['items'][2]['label']);
+        $this->assertEquals(array('articleId' => 3) , $dashboardGroupsSettings['sonata_group_one']['items'][2]['route_params']);
         $this->assertContains('sonata_news_admin', $dashboardGroupsSettings['sonata_group_one']['item_adds']);
         $this->assertContains('ROLE_ONE', $dashboardGroupsSettings['sonata_group_one']['roles']);
     }
@@ -119,7 +142,7 @@ class AddDependencyCallsCompilerPassTest extends \PHPUnit_Framework_TestCase
         $this->assertArrayHasKey('roles', $adminGroups['sonata_group_one']);
         $this->assertEquals('Group One Label', $adminGroups['sonata_group_one']['label']);
         $this->assertEquals('SonataAdminBundle', $adminGroups['sonata_group_one']['label_catalogue']);
-        $this->assertContains('sonata_post_admin', $adminGroups['sonata_group_one']['items']);
+        $this->assertContains('sonata_post_admin', $adminGroups['sonata_group_one']['items'][0]['admin']);
         $this->assertContains('sonata_news_admin', $adminGroups['sonata_group_one']['items']);
         $this->assertContains('sonata_news_admin', $adminGroups['sonata_group_one']['item_adds']);
         $this->assertFalse(in_array('sonata_article_admin', $adminGroups['sonata_group_one']['items']));
@@ -247,7 +270,16 @@ class AddDependencyCallsCompilerPassTest extends \PHPUnit_Framework_TestCase
                         'label' => 'Group One Label',
                         'label_catalogue' => 'SonataAdminBundle',
                         'items' => array(
-                            'sonata_post_admin'
+                            'sonata_post_admin',
+                            array(
+                                'route' => 'blog_name',
+                                'label' => 'Blog'
+                            ),
+                            array(
+                                'route'        => 'blog_article',
+                                'label'        => 'Article',
+                                'route_params' => array('articleId' => 3)
+                            ),
                         ),
                         'item_adds' => array(
                             'sonata_news_admin'

+ 92 - 2
Tests/DependencyInjection/ConfigurationTest.php

@@ -49,8 +49,8 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase
             'admin_services' => array(
                 'my_admin_id' => array(
                     'templates' => array(
-                        'form' => array('form.twig.html', 'form_extra.twig.html'),
-                        'view' => array('user_block' => 'SonataAdminBundle:mycustomtemplate.html.twig'),
+                        'form'   => array('form.twig.html', 'form_extra.twig.html'),
+                        'view'   => array('user_block' => 'SonataAdminBundle:mycustomtemplate.html.twig'),
                         'filter' => array()
                     )
                 )
@@ -115,4 +115,94 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase
 
         $this->assertEquals($config['dashboard']["blocks"][0]["roles"], array("ROLE_ADMIN"));
     }
+
+    public function testDashboardGroups()
+    {
+        $processor = new Processor();
+        $config = $processor->processConfiguration(new Configuration(), array(array(
+            "dashboard" => array(
+                "groups" => array(
+                    "bar" => array(
+                        "label" => "foo",
+                        "icon"  => '<i class="fa fa-edit"></i>',
+                        "items" => array(
+                            "item1",
+                            "item2",
+                            array(
+                                "label" => "fooLabel",
+                                "route" => "fooRoute",
+                                "route_params" => array("bar" => "foo")
+                            ),
+                            array(
+                                "label" => "barLabel",
+                                "route" => "barRoute",
+                            )
+                        )
+                    )
+                )
+            )
+        )));
+
+        $this->assertCount(4, $config['dashboard']["groups"]["bar"]["items"]);
+        $this->assertEquals(
+            $config['dashboard']["groups"]["bar"]["items"][0],
+            array(
+                "admin"        => "item1",
+                "route"        => "",
+                "route_params" => array(),
+                "label"        => "",
+            )
+        );
+        $this->assertEquals(
+            $config['dashboard']["groups"]["bar"]["items"][1],
+            array(
+                "admin"        => "item2",
+                "route"        => "",
+                "route_params" => array(),
+                "label"        => "",
+            )
+        );
+        $this->assertEquals(
+            $config['dashboard']["groups"]["bar"]["items"][2],
+            array(
+                "admin"        => "",
+                "route"        => "fooRoute",
+                "route_params" => array("bar" => "foo"),
+                "label"        => "fooLabel",
+            )
+        );
+        $this->assertEquals(
+            $config['dashboard']["groups"]["bar"]["items"][3],
+            array(
+                "admin"        => "",
+                "route"        => "barRoute",
+                "route_params" => array(),
+                "label"        => "barLabel",
+            )
+        );
+    }
+
+    public function testDashboardGroupsWithBadItemsParams()
+    {
+        $this->setExpectedException('\InvalidArgumentException', 'Expected either parameters "route" and "label" for array items');
+
+        $processor = new Processor();
+        $config = $processor->processConfiguration(new Configuration(), array(array(
+            "dashboard" => array(
+                "groups" => array(
+                    "bar" => array(
+                        "label" => "foo",
+                        "icon" => '<i class="fa fa-edit"></i>',
+                        "items" => array(
+                            "item1",
+                            "item2",
+                            array(
+                                "route" => "fooRoute"
+                            )
+                        )
+                    )
+                )
+            )
+        )));
+    }
 }