diff --git a/lib/redmine/menu_manager.rb b/lib/redmine/menu_manager.rb index debcdd143..320d4a655 100644 --- a/lib/redmine/menu_manager.rb +++ b/lib/redmine/menu_manager.rb @@ -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) - caption, url, selected = extract_node_details(node, project) - if node.hasChildren? - html = [] - html << '
  • ' - html << render_single_menu_node(node, caption, url, selected) # parent - html << ' ' - html << '
  • ' - return html.join("\n") + 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) + + html = returning [] do |html| + html << '
  • ' + # 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 << 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 << '
  • ' + 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 diff --git a/test/unit/lib/redmine/menu_manager/menu_helper_test.rb b/test/unit/lib/redmine/menu_manager/menu_helper_test.rb index 6f259f425..2d4778cbe 100644 --- a/test/unit/lib/redmine/menu_manager/menu_helper_test.rb +++ b/test/unit/lib/redmine/menu_manager/menu_helper_test.rb @@ -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| diff --git a/test/unit/lib/redmine/menu_manager/menu_item_test.rb b/test/unit/lib/redmine/menu_manager/menu_item_test.rb index ee302fc00..ccb52df6d 100644 --- a/test/unit/lib/redmine/menu_manager/menu_item_test.rb +++ b/test/unit/lib/redmine/menu_manager/menu_item_test.rb @@ -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 }) diff --git a/test/unit/lib/redmine/menu_manager_test.rb b/test/unit/lib/redmine/menu_manager_test.rb index 8c6ecda92..0c01ca323 100644 --- a/test/unit/lib/redmine/menu_manager_test.rb +++ b/test/unit/lib/redmine/menu_manager_test.rb @@ -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