]> source.dussan.org Git - redmine.git/commitdiff
Add support for unattached menus (generated dynamically)
authorEric Davis <edavis@littlestreamsoftware.com>
Wed, 25 Nov 2009 05:36:50 +0000 (05:36 +0000)
committerEric Davis <edavis@littlestreamsoftware.com>
Wed, 25 Nov 2009 05:36:50 +0000 (05:36 +0000)
A MenuItem can define a :child_menus option with a Proc.  When the menus
are rendered, the Proc will be run and the resulting MenuItems will be
added to the page as child menus

  #4250

git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@3091 e93f8b46-1217-0410-a6f0-8f06a7374b81

lib/redmine/menu_manager.rb
test/unit/lib/redmine/menu_manager/menu_helper_test.rb
test/unit/lib/redmine/menu_manager/menu_item_test.rb
test/unit/lib/redmine/menu_manager_test.rb

index debcdd143e136fbb365ecbae424da9c4fbaec347..320d4a6554079a31c2b371f3efe13e3f57043378 100644 (file)
@@ -95,6 +95,9 @@ Tree::TreeNode.send(:include, TreeNodePatch)
 
 module Redmine
   module MenuManager
+    class MenuError < StandardError #:nodoc:
+    end
+    
     module MenuController
       def self.included(base)
         base.extend(ClassMethods)
@@ -164,27 +167,71 @@ module Redmine
       end
 
       def render_menu_node(node, project=nil)
+        if node.hasChildren? || !node.child_menus.nil?
+          return render_menu_node_with_children(node, project)
+        else
+          caption, url, selected = extract_node_details(node, project)
+          return content_tag('li',
+                               render_single_menu_node(node, caption, url, selected))
+        end
+      end
+
+      def render_menu_node_with_children(node, project=nil)
         caption, url, selected = extract_node_details(node, project)
-        if node.hasChildren?
-          html = []
+
+        html = returning [] do |html|
           html << '<li>'
-          html << render_single_menu_node(node, caption, url, selected) # parent
-          html << '  <ul>'
-          node.children.each do |child|
-            html << render_menu_node(child, project)
+          # Parent
+          html << render_single_menu_node(node, caption, url, selected)
+
+          # Standard children
+          standard_children_list = returning "" do |child_html|
+            node.children.each do |child|
+              child_html << render_menu_node(child, project)
+            end
           end
-          html << '  </ul>'
+
+          html << content_tag(:ul, standard_children_list, :class => 'menu-children') unless standard_children_list.empty?
+
+          # Unattached children
+          unattached_children_list = render_unattached_children_menu(node, project)
+          html << content_tag(:ul, unattached_children_list, :class => 'menu-children unattached') unless unattached_children_list.blank?
+
           html << '</li>'
-          return html.join("\n")
-        else
-          return content_tag('li',
-                               render_single_menu_node(node, caption, url, selected))
+        end
+        return html.join("\n")
+      end
+
+      # Returns a list of unattached children menu items
+      def render_unattached_children_menu(node, project)
+        return nil unless node.child_menus
+
+        returning "" do |child_html|
+          unattached_children = node.child_menus.call(project)
+          # Tree nodes support #each so we need to do object detection
+          if unattached_children.is_a? Array
+            unattached_children.each do |child|
+              child_html << content_tag(:li, render_unattached_menu_item(child, project)) 
+            end
+          else
+            raise MenuError, ":child_menus must be an array of MenuItems"
+          end
         end
       end
 
       def render_single_menu_node(item, caption, url, selected)
         link_to(h(caption), url, item.html_options(:selected => selected))
       end
+
+      def render_unattached_menu_item(menu_item, project)
+        raise MenuError, ":child_menus must be an array of MenuItems" unless menu_item.is_a? MenuItem
+
+        if User.current.allowed_to?(menu_item.url, project)
+          link_to(h(menu_item.caption),
+                  menu_item.url,
+                  menu_item.html_options)
+        end
+      end
       
       def menu_items_for(menu, project=nil)
         items = []
@@ -336,12 +383,13 @@ module Redmine
     
     class MenuItem < Tree::TreeNode
       include Redmine::I18n
-      attr_reader :name, :url, :param, :condition, :parent_menu
+      attr_reader :name, :url, :param, :condition, :parent_menu, :child_menus
       
       def initialize(name, url, options)
         raise ArgumentError, "Invalid option :if for menu item '#{name}'" if options[:if] && !options[:if].respond_to?(:call)
         raise ArgumentError, "Invalid option :html for menu item '#{name}'" if options[:html] && !options[:html].is_a?(Hash)
         raise ArgumentError, "Cannot set the :parent_menu to be the same as this item" if options[:parent_menu] == name.to_sym
+        raise ArgumentError, "Invalid option :child_menus for menu item '#{name}'" if options[:child_menus] && !options[:child_menus].respond_to?(:call)
         @name = name
         @url = url
         @condition = options[:if]
@@ -351,6 +399,7 @@ module Redmine
         # Adds a unique class to each menu item based on its name
         @html_options[:class] = [@html_options[:class], @name.to_s.dasherize].compact.join(' ')
         @parent_menu = options[:parent_menu]
+        @child_menus = options[:child_menus]
         super @name.to_sym
       end
       
index 6f259f4259c730798578084c5f27b367cdea888a..2d4778cbe130a739eb6e092445b8f145cf8da7ed 100644 (file)
@@ -101,6 +101,107 @@ class Redmine::MenuManager::MenuHelperTest < HelperTestCase
     
   end
 
+  def test_render_menu_node_with_child_menus
+    User.current = User.find(2)
+    
+    parent_node = Redmine::MenuManager::MenuItem.new(:parent_node,
+                                                     '/test',
+                                                     {
+                                                       :child_menus => Proc.new {|p|
+                                                         child_menus = []
+                                                         3.times do |time|
+                                                           child_menus << Redmine::MenuManager::MenuItem.new("test_child_#{time}",
+                                                                                                             {:controller => 'issues', :action => 'index'},
+                                                                                                             {})
+                                                         end
+                                                         child_menus
+                                                       }
+                                                     })
+    @response.body = render_menu_node(parent_node, Project.find(1))
+
+    assert_select("li") do
+      assert_select("a.parent-node", "Parent node")
+      assert_select("ul") do
+        assert_select("li a.test-child-0", "Test child 0")
+        assert_select("li a.test-child-1", "Test child 1")
+        assert_select("li a.test-child-2", "Test child 2")
+      end
+    end
+  end
+
+  def test_render_menu_node_with_nested_items_and_child_menus
+    User.current = User.find(2)
+
+    parent_node = Redmine::MenuManager::MenuItem.new(:parent_node,
+                                                     '/test',
+                                                     {
+                                                       :child_menus => Proc.new {|p|
+                                                         child_menus = []
+                                                         3.times do |time|
+                                                           child_menus << Redmine::MenuManager::MenuItem.new("test_child_#{time}", {:controller => 'issues', :action => 'index'}, {})
+                                                         end
+                                                         child_menus
+                                                       }
+                                                     })
+
+    parent_node << Redmine::MenuManager::MenuItem.new(:child_node,
+                                                     '/test',
+                                                     {
+                                                       :child_menus => Proc.new {|p|
+                                                         child_menus = []
+                                                         6.times do |time|
+                                                            child_menus << Redmine::MenuManager::MenuItem.new("test_dynamic_child_#{time}", {:controller => 'issues', :action => 'index'}, {})
+                                                         end
+                                                         child_menus
+                                                       }
+                                                     })
+
+    @response.body = render_menu_node(parent_node, Project.find(1))
+
+    assert_select("li") do
+      assert_select("a.parent-node", "Parent node")
+      assert_select("ul") do
+        assert_select("li a.child-node", "Child node")
+        assert_select("ul") do
+          assert_select("li a.test-dynamic-child-0", "Test dynamic child 0")
+          assert_select("li a.test-dynamic-child-1", "Test dynamic child 1")
+          assert_select("li a.test-dynamic-child-2", "Test dynamic child 2")
+          assert_select("li a.test-dynamic-child-3", "Test dynamic child 3")
+          assert_select("li a.test-dynamic-child-4", "Test dynamic child 4")
+          assert_select("li a.test-dynamic-child-5", "Test dynamic child 5")
+        end
+        assert_select("li a.test-child-0", "Test child 0")
+        assert_select("li a.test-child-1", "Test child 1")
+        assert_select("li a.test-child-2", "Test child 2")
+      end
+    end
+  end
+
+  def test_render_menu_node_with_child_menus_without_an_array
+    parent_node = Redmine::MenuManager::MenuItem.new(:parent_node,
+                                                     '/test',
+                                                     {
+                                                       :child_menus => Proc.new {|p| Redmine::MenuManager::MenuItem.new("test_child", "/testing", {})}
+                                                     })
+
+    assert_raises Redmine::MenuManager::MenuError, ":child_menus must be an array of MenuItems" do
+      @response.body = render_menu_node(parent_node, Project.find(1))
+    end
+  end
+    
+  def test_render_menu_node_with_incorrect_child_menus
+    parent_node = Redmine::MenuManager::MenuItem.new(:parent_node,
+                                                     '/test',
+                                                     {
+                                                       :child_menus => Proc.new {|p| ["a string"] }
+                                                     })
+
+    assert_raises Redmine::MenuManager::MenuError, ":child_menus must be an array of MenuItems" do
+      @response.body = render_menu_node(parent_node, Project.find(1))
+    end
+
+  end
+
   def test_menu_items_for_should_yield_all_items_if_passed_a_block
     menu_name = :test_menu_items_for_should_yield_all_items_if_passed_a_block
     Redmine::MenuManager.map menu_name do |menu|
index ee302fc0054d347d5d0cbcd0b41b54e724e8f93d..ccb52df6d76714666ac528b797ebca4abccf36a6 100644 (file)
@@ -92,6 +92,20 @@ class Redmine::MenuManager::MenuItemTest < Test::Unit::TestCase
                                               })
   end
 
+  def test_new_menu_item_should_require_a_proc_to_use_the_child_menus_option
+    assert_raises ArgumentError do
+      Redmine::MenuManager::MenuItem.new(:test_error, '/test',
+                                         {
+                                           :child_menus => ['not_a_proc']
+                                         })
+    end
+
+    assert Redmine::MenuManager::MenuItem.new(:test_good_child_menus, '/test',
+                                              {
+                                                :child_menus => Proc.new{}
+                                              })
+  end
+
   def test_new_should_not_allow_setting_the_parent_menu_item_to_the_current_item
     assert_raises ArgumentError do
       Redmine::MenuManager::MenuItem.new(:test_error, '/test', { :parent_menu => :test_error })
index 8c6ecda929da20e8218b738ef12d522d662580e0..0c01ca323694da9061a14ef07d75d9ed1fb7dc7a 100644 (file)
@@ -25,4 +25,8 @@ class Redmine::MenuManagerTest < Test::Unit::TestCase
   context "MenuManager#items" do
     should "be tested"
   end
+
+  should "be tested" do
+    assert true
+  end
 end