docbrown/app/lib/menu.rb
Jeremy Friesen 71e9f2bfb4
Refactoring AdminMenu and Menu towards classes (#16847)
As I was looking to implement the new Spaces feature (see
forem/forem#16842), I began looking at how to adjust the admin menu.  I
found the Menu and AdminMenu.  I wanted to add the "spaces" item under
the `:content_manager` scope.  Looking at the implementation, I saw I
would need some additional branching logic.

To get an understanding of the implementation, I chose factor towards
classes.

This refactor does a few things:

1) Clarifies a method name (e.g. "children?" becomes
   "has_multiple_children?")
2) Adds some tests of the menu implementation.
3) Removes deeply nested hashs in favor of first class objects.
4) Removes a complex method for determining menu visibility, instead
   favoring a method that either takes a boolean OR a lambda.

There are further refactors to consider, especially in regards to the
helper methods and some of the coercion of strings into different
formats.  But this refactor gets me the part I most am interested in:

Making it easier to specify a menu item as visible or not.
2022-03-21 13:31:20 -04:00

97 lines
2.5 KiB
Ruby

# This class is responsible for helping to build a menu.
#
# @see {AdminMenu}
class Menu
# @return [Hash<Symbol, Menu::Scope>]
def self.define(&block)
builder = new
builder.instance_exec(&block)
builder.items
end
attr_reader :items
def initialize
@items = {}
end
# @param name [String]
# @param svg [String]
# @param children [Array<Hash>]
def scope(name, svg, children)
@items[name] = Scope.new(name: name, svg: "#{svg}.svg", children: children)
end
# @see {Menu::Item#initialize} for details on the parameters.
# @return [Menu::Item]
def item(...)
Item.new(...)
end
class Scope
attr_reader :name, :svg, :children
def initialize(name:, svg:, children:)
@name = name
@svg = svg
@children = children
end
# Does this Scope have multiple children? From a presentation stand point
# we render menu items with multiple children a bit differently than those
# with only one child.
#
# @return [TrueClass] when this item has more than 1 child.
# @return [FalseClass] when this item has one or less children.
def has_multiple_children?
@children.length > 1
end
# @return [TrueClass] when this item has at least one child.
# @return [FalseClass] when this item has no children.
def has_children?
!@children.empty?
end
end
class Item
attr_reader :name, :controller, :children, :parent
# @param name [String]
# @param controller [String]
# @param children [Array<Hash>]
# @param visible [Boolean, #call]
# @param parent [NilClass, String]
def initialize(name:, controller: name, children: [], visible: true, parent: nil)
@name = name
@controller = controller.tr(" ", "_")
@children = children
@visible = visible
@parent = parent
end
# @return [TrueClass] if this menu item should be visible
# @return [FalseClass] if this menu item should not be visible
def visible?
if @visible.respond_to?(:call)
@visible.call
else
@visible
end
end
alias visible visible?
# @return [TrueClass] when this item has more than 1 child.
# @return [FalseClass] when this item has one or less children.
def has_multiple_children?
@children.length > 1
end
# @return [TrueClass] when this item has at least one child.
# @return [FalseClass] when this item has no children.
def has_children?
!@children.empty?
end
end
end