Skip to content

Commit

Permalink
Add Alchemy::Admin::ToolbarButton ViewComponent
Browse files Browse the repository at this point in the history
  • Loading branch information
tvdeyen committed Mar 22, 2024
1 parent e7feafb commit d3cf6f3
Show file tree
Hide file tree
Showing 5 changed files with 285 additions and 114 deletions.
111 changes: 111 additions & 0 deletions app/components/alchemy/admin/toolbar_button.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
module Alchemy
module Admin
# Renders a toolbar button for the Alchemy toolbar
#
# == Example
#
# <%= render Alchemy::Admin::ToolbarButton.new(
# url: new_resource_path,
# icon: :plus,
# label: 'Create Resource',
# hotkey: 'alt+n',
# dialog_options: {
# title: 'Create Resource',
# size: "430x400"
# },
# if_permitted_to: [:create, resource_model]
# ) %>
#
# @param [String] :url
# Url for link.
# @param [String] :icon
# Icon name. See https://remixicon.com for available icons.
# @param [String] :label
# Text for button tooltip.
# @param [String] :hotkey
# Keyboard shortcut for this button. I.E +alt-n+
# @param [Hash] :dialog_options
# Overlay options. See link_to_dialog helper.
# @param [Array] :if_permitted_to ([:action, :controller])
# Check permission for button. Exactly how you defined the permission in your +authorization_rules.rb+. Defaults to controller and action from button url.
# @param [Boolean] :skip_permission_check (false)
# Skip the permission check. NOT RECOMMENDED!
#
class ToolbarButton < ViewComponent::Base
erb_template <<-ERB
<div class="toolbar_button">
<sl-tooltip content="<%= label %>" placement="<%= tooltip_placement %>">
<%= link_to(render_icon(icon, style: icon_style), url, {
class: css_classes,
"data-dialog-options" => dialog ? dialog_options.to_json : nil,
"data-alchemy-hotkey" => hotkey,
:is => dialog ? "alchemy-dialog-link" : nil
}.merge(link_options)) %>
</sl-tooltip>
</div>
ERB

delegate :can?, :link_to, :link_to_dialog, :render_icon, to: :helpers

attr_reader :url,
:icon,
:label,
:hotkey,
:dialog,
:dialog_options,
:skip_permission_check,
:if_permitted_to,
:active,
:link_options,
:icon_style,
:tooltip_placement

def initialize(
url:,
icon:,
label:,
hotkey: nil,
title: nil,
dialog: true,
dialog_options: {},
skip_permission_check: false,
if_permitted_to: [],
active: false,
link_options: {},
icon_style: "line",
tooltip_placement: "top-start"
)
@url = url
@icon = icon
@label = label
@hotkey = hotkey
@dialog = dialog
@dialog_options = dialog_options
@skip_permission_check = skip_permission_check
@if_permitted_to = if_permitted_to
@active = active
@link_options = link_options
@icon_style = icon_style
@tooltip_placement = tooltip_placement
end

def render?
skip_permission_check || can?(*permission_options)
end

private

def css_classes = ["icon_button", active && "active"].compact

def permission_options = if_permitted_to.presence || permissions_from_url

def permissions_from_url
action_controller = url.gsub(/\A\//, "").split("/")
[
action_controller.last.to_sym,
action_controller[0..action_controller.length - 2].join("_").to_sym
]
end
end
end
end
36 changes: 1 addition & 35 deletions app/helpers/alchemy/admin/base_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -268,23 +268,7 @@ def render_alchemy_title
# Shows the please wait dialog while loading. Only for buttons not opening an dialog.
#
def toolbar_button(options = {})
options = {
dialog: true,
skip_permission_check: false,
active: false,
link_options: {},
dialog_options: {},
loading_indicator: false
}.merge(options.symbolize_keys)
button = render(
"alchemy/admin/partials/toolbar_button",
options: options
)
if options[:skip_permission_check] || can?(*permission_from_options(options))
button
else
""
end
render Alchemy::Admin::ToolbarButton.new(**options)
end

# Renders a textfield ready to display a datepicker
Expand Down Expand Up @@ -396,24 +380,6 @@ def page_layout_missing_warning
Alchemy.t(:page_definition_missing)
)
end

private

def permission_from_options(options)
if options[:if_permitted_to].blank?
options[:if_permitted_to] = permission_array_from_url(options)
else
options[:if_permitted_to]
end
end

def permission_array_from_url(options)
action_controller = options[:url].gsub(/\A\//, "").split("/")
[
action_controller.last.to_sym,
action_controller[0..action_controller.length - 2].join("_").to_sym
]
end
end
end
end
29 changes: 0 additions & 29 deletions app/views/alchemy/admin/partials/_toolbar_button.html.erb

This file was deleted.

168 changes: 168 additions & 0 deletions spec/components/alchemy/admin/toolbar_button_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
require "rails_helper"

RSpec.describe Alchemy::Admin::ToolbarButton, type: :component do
before do
allow_any_instance_of(described_class).to receive(:render_icon) do |component|
Alchemy::Admin::Icon.new(component.icon, style: component.icon_style).call
end
end

let(:component) do
described_class.new(url: admin_dashboard_path, icon: "info", label: "Show Info")
end

context "with permission" do
before { expect(component).to receive(:can?) { true } }

it "renders a toolbar button" do
render_inline component
expect(page).to have_css %(sl-tooltip a.icon_button[href="#{admin_dashboard_path}"])
end

context "with dialog option set to false" do
let(:component) do
described_class.new(
url: admin_dashboard_path,
icon: "info",
label: "Show Info",
dialog: false
)
end

it "renders a normal link" do
render_inline component
expect(page).to have_css(%(a[href="#{admin_dashboard_path}"]))
expect(page).not_to have_css("[data-dialog-options]")
end
end

context "with dialog_options set" do
let(:component) do
described_class.new(
url: admin_dashboard_path,
icon: "info",
label: "Show Info",
dialog_options: {
title: "Info",
size: "300x200"
}
)
end

it "passes them to the link" do
render_inline component
expect(page).to have_css(%(a[data-dialog-options='{"title":"Info","size":"300x200"}']))
end
end

context "with hotkey set" do
let(:component) do
described_class.new(
url: admin_dashboard_path,
icon: "info",
label: "Show Info",
hotkey: "alt+i"
)
end

it "passes it to the link" do
render_inline component
expect(page).to have_css('a[data-alchemy-hotkey="alt+i"]')
end
end

context "with icon_style set" do
let(:component) do
described_class.new(
url: admin_dashboard_path,
icon: "info",
label: "Show Info",
icon_style: "fill"
)
end

it "passes it to the icon" do
render_inline component
expect(page).to have_css('alchemy-icon[icon-style="fill"]')
end
end

context "with tooltip_placement set" do
let(:component) do
described_class.new(
url: admin_dashboard_path,
icon: "info",
label: "Show Info",
tooltip_placement: "bottom-center"
)
end

it "passes it to the icon" do
render_inline component
expect(page).to have_css('sl-tooltip[placement="bottom-center"]')
end
end

context "with active set to true" do
let(:component) do
described_class.new(
url: admin_dashboard_path,
icon: "info",
label: "Show Info",
active: true
)
end

it "button has active class" do
render_inline component
expect(page).to have_css("a.active")
end
end
end

context "without permission" do
before { expect(component).to receive(:can?) { false } }

it "returns empty string" do
render_inline component
expect(page.native.inner_html).to be_empty
end
end

context "with disabled permission check" do
before { expect(component).not_to receive(:can?) { false } }

let(:component) do
described_class.new(
url: admin_dashboard_path,
icon: "info",
label: "Show Info",
skip_permission_check: true
)
end

it "renders a toolbar button" do
render_inline component
expect(page).to have_css %(sl-tooltip .icon_button[href="#{admin_dashboard_path}"])
end
end

context "with empty permission option" do
before { expect(component).to receive(:can?) { true } }

let(:component) do
described_class.new(
url: admin_dashboard_path,
icon: "info",
label: "Show Info",
if_permitted_to: ""
)
end

it "returns reads the permission from url" do
expect(component).to receive(:permissions_from_url)
render_inline component
expect(page).to have_css %(sl-tooltip .icon_button[href="#{admin_dashboard_path}"])
end
end
end
Loading

0 comments on commit d3cf6f3

Please sign in to comment.