Commit 57ff3fe7 authored by Arturo Herrero's avatar Arturo Herrero

Merge branch '326304-fj-refactor-existing-sidebar-rendering-logic' into 'master'

Drafting the new architecture for sidebars

See merge request gitlab-org/gitlab!57811
parents 02f78e50 504b4ce5
# frozen_string_literal: true
module Sidebars
module ContainerWithHtmlOptions
# The attributes returned from this method
# will be applied to helper methods like
# `link_to` or the div containing the container.
def container_html_options
{
title: title
}.merge(extra_container_html_options)
end
# Classes will override mostly this method
# and not `container_html_options`.
def extra_container_html_options
{}
end
def title
raise NotImplementedError
end
# The attributes returned from this method
# will be applied right next to the title,
# for example in the span that renders the title.
def title_html_options
{}
end
def link
raise NotImplementedError
end
end
end
# frozen_string_literal: true
module Sidebars
module HasActiveRoutes
# This method will indicate for which paths or
# controllers, the menu or menu item should
# be set as active.
#
# The returned values are passed to the `nav_link` helper method,
# so the params can be either `path`, `page`, `controller`.
# Param 'action' is not supported.
def active_routes
{}
end
end
end
# frozen_string_literal: true
# This module has the necessary methods to store
# hints for menus. Hints are elements displayed
# when the user hover the menu item.
module Sidebars
module HasHint
def show_hint?
false
end
def hint_html_options
{}
end
end
end
# frozen_string_literal: true
# This module has the necessary methods to show
# sprites or images next to the menu item.
module Sidebars
module HasIcon
def sprite_icon
nil
end
def sprite_icon_html_options
{}
end
def image_path
nil
end
def image_html_options
{}
end
def icon_or_image?
sprite_icon || image_path
end
end
end
# frozen_string_literal: true
# This module introduces the logic to show the "pill" element
# next to the menu item, indicating the a count.
module Sidebars
module HasPill
def has_pill?
false
end
# In this method we will need to provide the query
# to retrieve the elements count
def pill_count
raise NotImplementedError
end
def pill_html_options
{}
end
end
end
# frozen_string_literal: true
# This module handles elements in a list. All elements
# must have a different class
module Sidebars
module PositionableList
def add_element(list, element)
list << element
end
def insert_element_before(list, before_element, new_element)
index = index_of(list, before_element)
if index
list.insert(index, new_element)
else
list.unshift(new_element)
end
end
def insert_element_after(list, after_element, new_element)
index = index_of(list, after_element)
if index
list.insert(index + 1, new_element)
else
add_element(list, new_element)
end
end
private
def index_of(list, element)
list.index { |e| e.is_a?(element) }
end
end
end
# frozen_string_literal: true
module Sidebars
module Renderable
# This method will control whether the menu or menu_item
# should be rendered. It will be overriden by specific
# classes.
def render?
true
end
end
end
# frozen_string_literal: true
# This class stores all the information needed to display and
# render the sidebar and menus.
# It usually stores information regarding the context and calculated
# values where the logic is in helpers.
module Sidebars
class Context
attr_reader :current_user, :container
def initialize(current_user:, container:, **args)
@current_user = current_user
@container = container
args.each do |key, value|
singleton_class.public_send(:attr_reader, key) # rubocop:disable GitlabSecurity/PublicSend
instance_variable_set("@#{key}", value)
end
end
end
end
# frozen_string_literal: true
module Sidebars
class Menu
extend ::Gitlab::Utils::Override
include ::Gitlab::Routing
include GitlabRoutingHelper
include Gitlab::Allowable
include ::Sidebars::HasPill
include ::Sidebars::HasIcon
include ::Sidebars::PositionableList
include ::Sidebars::Renderable
include ::Sidebars::ContainerWithHtmlOptions
include ::Sidebars::HasActiveRoutes
attr_reader :context
delegate :current_user, :container, to: :@context
def initialize(context)
@context = context
@items = []
configure_menu_items
end
def configure_menu_items
# No-op
end
override :render?
def render?
@items.empty? || renderable_items.any?
end
# Menus might have or not a link
override :link
def link
nil
end
# This method normalizes the information retrieved from the submenus and this menu
# Value from menus is something like: [{ path: 'foo', path: 'bar', controller: :foo }]
# This method filters the information and returns: { path: ['foo', 'bar'], controller: :foo }
def all_active_routes
@all_active_routes ||= begin
([active_routes] + renderable_items.map(&:active_routes)).flatten.each_with_object({}) do |pairs, hash|
pairs.each do |k, v|
hash[k] ||= []
hash[k] += Array(v)
hash[k].uniq!
end
hash
end
end
end
def has_items?
@items.any?
end
def add_item(item)
add_element(@items, item)
end
def insert_item_before(before_item, new_item)
insert_element_before(@items, before_item, new_item)
end
def insert_item_after(after_item, new_item)
insert_element_after(@items, after_item, new_item)
end
def has_renderable_items?
renderable_items.any?
end
def renderable_items
@renderable_items ||= @items.select(&:render?)
end
end
end
# frozen_string_literal: true
module Sidebars
class MenuItem
extend ::Gitlab::Utils::Override
include ::Gitlab::Routing
include GitlabRoutingHelper
include Gitlab::Allowable
include ::Sidebars::HasIcon
include ::Sidebars::HasHint
include ::Sidebars::Renderable
include ::Sidebars::ContainerWithHtmlOptions
include ::Sidebars::HasActiveRoutes
attr_reader :context
def initialize(context)
@context = context
end
end
end
# frozen_string_literal: true
module Sidebars
class Panel
extend ::Gitlab::Utils::Override
include ::Sidebars::PositionableList
attr_reader :context, :scope_menu, :hidden_menu
def initialize(context)
@context = context
@scope_menu = nil
@hidden_menu = nil
@menus = []
configure_menus
end
def configure_menus
# No-op
end
def add_menu(menu)
add_element(@menus, menu)
end
def insert_menu_before(before_menu, new_menu)
insert_element_before(@menus, before_menu, new_menu)
end
def insert_menu_after(after_menu, new_menu)
insert_element_after(@menus, after_menu, new_menu)
end
def set_scope_menu(scope_menu)
@scope_menu = scope_menu
end
def set_hidden_menu(hidden_menu)
@hidden_menu = hidden_menu
end
def aria_label
raise NotImplementedError
end
def has_renderable_menus?
renderable_menus.any?
end
def renderable_menus
@renderable_menus ||= @menus.select(&:render?)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::ContainerWithHtmlOptions do
subject do
Class.new do
include Sidebars::ContainerWithHtmlOptions
def title
'Foo'
end
end.new
end
describe '#container_html_options' do
it 'includes title attribute' do
expect(subject.container_html_options).to eq(title: 'Foo')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::PositionableList do
subject do
Class.new do
include Sidebars::PositionableList
end.new
end
describe '#add_element' do
it 'adds the element to the last position of the list' do
list = [1, 2]
subject.add_element(list, 3)
expect(list).to eq([1, 2, 3])
end
end
describe '#insert_element_before' do
let(:user) { build(:user) }
let(:list) { [1, user] }
it 'adds element before the specific element class' do
subject.insert_element_before(list, User, 2)
expect(list).to eq [1, 2, user]
end
context 'when reference element does not exist' do
it 'adds the element to the top of the list' do
subject.insert_element_before(list, Project, 2)
expect(list).to eq [2, 1, user]
end
end
end
describe '#insert_element_after' do
let(:user) { build(:user) }
let(:list) { [1, user] }
it 'adds element after the specific element class' do
subject.insert_element_after(list, Integer, 2)
expect(list).to eq [1, 2, user]
end
context 'when reference element does not exist' do
it 'adds the element to the end of the list' do
subject.insert_element_after(list, Project, 2)
expect(list).to eq [1, user, 2]
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::Menu do
let(:menu) { described_class.new(context) }
let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
describe '#all_active_routes' do
it 'gathers all active routes of items and the current menu' do
menu_item1 = Sidebars::MenuItem.new(context)
menu_item2 = Sidebars::MenuItem.new(context)
menu_item3 = Sidebars::MenuItem.new(context)
menu.add_item(menu_item1)
menu.add_item(menu_item2)
menu.add_item(menu_item3)
allow(menu).to receive(:active_routes).and_return({ path: 'foo' })
allow(menu_item1).to receive(:active_routes).and_return({ path: %w(bar test) })
allow(menu_item2).to receive(:active_routes).and_return({ controller: 'fooc' })
allow(menu_item3).to receive(:active_routes).and_return({ controller: 'barc' })
expect(menu.all_active_routes).to eq({ path: %w(foo bar test), controller: %w(fooc barc) })
end
it 'does not include routes for non renderable items' do
menu_item = Sidebars::MenuItem.new(context)
menu.add_item(menu_item)
allow(menu).to receive(:active_routes).and_return({ path: 'foo' })
allow(menu_item).to receive(:render?).and_return(false)
allow(menu_item).to receive(:active_routes).and_return({ controller: 'bar' })
expect(menu.all_active_routes).to eq({ path: ['foo'] })
end
end
describe '#render?' do
context 'when the menus has no items' do
it 'returns true' do
expect(menu.render?).to be true
end
end
context 'when the menu has items' do
let(:menu_item) { Sidebars::MenuItem.new(context) }
before do
menu.add_item(menu_item)
end
context 'when items are not renderable' do
it 'returns false' do
allow(menu_item).to receive(:render?).and_return(false)
expect(menu.render?).to be false
end
end
context 'when there are renderable items' do
it 'returns true' do
expect(menu.render?).to be true
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::Panel do
let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
let(:panel) { Sidebars::Panel.new(context) }
let(:menu1) { Sidebars::Menu.new(context) }
let(:menu2) { Sidebars::Menu.new(context) }
describe '#renderable_menus' do
it 'returns only renderable menus' do
panel.add_menu(menu1)
panel.add_menu(menu2)
allow(menu1).to receive(:render?).and_return(true)
allow(menu2).to receive(:render?).and_return(false)
expect(panel.renderable_menus).to eq([menu1])
end
end
describe '#has_renderable_menus?' do
it 'returns false when no renderable menus' do
expect(panel.has_renderable_menus?).to be false
end
it 'returns true when no renderable menus' do
panel.add_menu(menu1)
expect(panel.has_renderable_menus?).to be true
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment