Skip to content

Commit

Permalink
Add conditions and inheritance
Browse files Browse the repository at this point in the history
  • Loading branch information
jaynetics committed Jul 2, 2024
1 parent 3766254 commit b3844f5
Show file tree
Hide file tree
Showing 18 changed files with 275 additions and 60 deletions.
42 changes: 33 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,14 @@ end
Render it somewhere. E.g. `app/views/layouts/application.html.erb`:

```erb
<%= Omnibar.render %>
<%= Omnibar.render(self) %>
```

If you have a fully decoupled frontend, use `Omnibar.html_url` instead, fetch the omnibar HTML from there, and inject it.

### Authorization

You can limit access to commands (e.g. search commands). This will not limit access to plain items.
You can limit access to commands (e.g. search commands) and items.

#### Option 1: globally limit engine access

Expand All @@ -119,19 +119,35 @@ MyOmnibar.auth = ->(controller, omnibar:) do
end
```

#### Option 3: Item-level conditionality

Items and commands can have an `if` proc that is executed in the controller context. If it returns false, the item is not shown and the command is not executed.

```ruby
MyOmnibar.add_item(
title: 'Admin link',
url: admin_url,
if: ->{ current_user.admin? }
)
```

For this to work, the controller context must be given to the omnibar when rendering (e.g. by passing `self` in a view):

```erb
<%= Omnibar.render(self) %>
```

### Using multiple different omnibars

```ruby
BaseOmnibar = Class.new(RailsOmnibar)
BaseOmnibar.configure do |c|
BaseOmnibar = Class.new(RailsOmnibar).configure do |c|
c.add_item(
title: 'Log in',
url: Rails.application.routes.url_helpers.log_in_url
)
end

UserOmnibar = Class.new(RailsOmnibar)
UserOmnibar.configure do |c|
UserOmnibar = Class.new(RailsOmnibar).configure do |c|
c.auth = ->{ user_signed_in? }
c.add_item(
title: 'Log out',
Expand All @@ -143,7 +159,15 @@ end
Then, in some layout:

```erb
<%= (user_signed_in? ? UserOmnibar : BaseOmnibar).render %>
<%= (user_signed_in? ? UserOmnibar : BaseOmnibar).render(self) %>
```

Omnibars can also inherit commands, configuration, and items from existing omnibars:

```ruby
SuperuserOmnibar = Class.new(UserOmnibar).configure do |c|
# add more items that only superusers should get
end
```

### Other options and usage patterns
Expand Down Expand Up @@ -172,15 +196,15 @@ end
```ruby
module AddOmnibar
def build_page(...)
within(super) { text_node(MyOmnibar.render) }
within(super) { text_node(MyOmnibar.render(self)) }
end
end
ActiveAdmin::Views::Pages::Base.prepend(AddOmnibar)
```

##### To render in RailsAdmin

Add `MyOmnibar.render` to `app/views/layouts/rails_admin/application.*`.
Add `MyOmnibar.render(self)` to `app/views/layouts/rails_admin/application.*`.

#### Adding all index routes as searchable items

Expand Down
2 changes: 1 addition & 1 deletion app/controllers/rails_omnibar/html_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class RailsOmnibar::HtmlController < RailsOmnibar::BaseController
def show
render html: omnibar.render
render html: omnibar.render(self)
end
end
1 change: 1 addition & 0 deletions bin/console
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require 'bundler/setup'
require 'rails_omnibar'
require_relative '../spec/dummy/config/environment'

require 'irb'
IRB.start(__FILE__)
11 changes: 9 additions & 2 deletions lib/rails_omnibar/command/base.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
class RailsOmnibar
module Command
class Base
attr_reader :pattern, :resolver, :description, :example
attr_reader :pattern, :resolver, :description, :example, :if

def initialize(pattern:, resolver:, description: nil, example: nil)
def initialize(pattern:, resolver:, description: nil, example: nil, if: nil)
@pattern = cast_to_pattern(pattern)
@resolver = RailsOmnibar.cast_to_proc(resolver)
@description = description
@example = example
@if = RailsOmnibar.cast_to_condition(binding.local_variable_get(:if))
end

def call(input, controller:, omnibar:)
Expand All @@ -19,6 +20,12 @@ def call(input, controller:, omnibar:)
results.map { |e| RailsOmnibar.cast_to_item(e) }
end

def handle?(input, controller:, omnibar:)
return false unless pattern.match?(input)

RailsOmnibar.evaluate_condition(self.if, context: controller, omnibar: omnibar)
end

private

def cast_to_pattern(arg)
Expand Down
3 changes: 2 additions & 1 deletion lib/rails_omnibar/command/search.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def initialize(finder:, itemizer:, **kwargs)

# ActiveRecord-specific search.
class RecordSearch < Search
def initialize(model:, columns: :id, pattern: nil, finder: nil, itemizer: nil, example: nil)
def initialize(model:, columns: :id, pattern: nil, finder: nil, itemizer: nil, example: nil, if: nil)
# casting and validations
model = model.to_s.classify.constantize unless model.is_a?(Class)
model < ActiveRecord::Base || raise(ArgumentError, 'model: must be a model')
Expand Down Expand Up @@ -64,6 +64,7 @@ def initialize(model:, columns: :id, pattern: nil, finder: nil, itemizer: nil, e
pattern: pattern,
finder: finder,
itemizer: itemizer,
if: binding.local_variable_get(:if),
)
end
end
Expand Down
23 changes: 16 additions & 7 deletions lib/rails_omnibar/commands.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
class RailsOmnibar
def handle(input, controller)
handler = commands.find { |h| h.pattern.match?(input) }
handler = commands.find do |cmd|
cmd.handle?(input, controller: controller, omnibar: self)
end
handler&.call(input, controller: controller, omnibar: self) || []
end

def command_pattern
commands.any? ? Regexp.union(commands.map(&:pattern)) : /$NO_COMMANDS/
end

def add_command(command)
check_const_and_clear_cache
commands << RailsOmnibar.cast_to_command(command)
self
clear_command_pattern_cache
self.class
end

def self.cast_to_command(arg)
Expand Down Expand Up @@ -43,6 +41,17 @@ def self.cast_to_proc(arg)

private

def command_pattern
@command_pattern ||= begin
re = commands.any? ? Regexp.union(commands.map(&:pattern)) : /$NO_COMMANDS/
JsRegex.new!(re, target: 'ES2018')
end
end

def clear_command_pattern_cache
@command_pattern = nil
end

def commands
@commands ||= []
end
Expand Down
27 changes: 27 additions & 0 deletions lib/rails_omnibar/conditions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class RailsOmnibar
def self.cast_to_condition(arg)
case arg
when nil, true, false then arg
else
arg.try(:arity) == 0 ? arg : RailsOmnibar.cast_to_proc(arg)
end
end

def self.evaluate_condition(condition, context:, omnibar:)
case condition
when nil, true then true
when false then false
else
context || raise(<<~EOS)
Missing context for condition, please render the omnibar with `.render(self)`
EOS
if condition.try(:arity) == 0
context.instance_exec(&condition)
elsif condition.respond_to?(:call)
condition.call(context, controller: context, omnibar: omnibar)
else
raise("unsupported condition type: #{condition.class}")
end
end
end
end
48 changes: 29 additions & 19 deletions lib/rails_omnibar/config.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
class RailsOmnibar
def configure(&block)
check_const_and_clear_cache
tap(&block)
self.class
end

attr_reader :auth
def auth=(arg)
@auth = arg.try(:arity) == 0 ? arg : RailsOmnibar.cast_to_proc(arg)
config[:auth] = arg.try(:arity) == 0 ? arg : RailsOmnibar.cast_to_proc(arg)
end
def auth
config[:auth]
end
def authorize(controller)
if auth.nil?
Expand All @@ -20,47 +22,55 @@ def authorize(controller)

def max_results=(arg)
arg.is_a?(Integer) && arg > 0 || raise(ArgumentError, 'max_results must be > 0')
@max_results = arg
config[:max_results] = arg
end
def max_results
@max_results || 10
config[:max_results] || 10
end

attr_writer :modal
def modal=(arg)
config[:modal] = arg
end
def modal?
instance_variable_defined?(:@modal) ? !!@modal : false
config.key?(:modal) ? !!config[:modal] : false
end

attr_writer :calculator
def calculator=(arg)
config[:calculator] = arg
end
def calculator?
instance_variable_defined?(:@calculator) ? !!@calculator : true
config.key?(:calculator) ? !!config[:calculator] : true
end

def hotkey
@hotkey || 'k'
end
def hotkey=(arg)
arg.to_s.size == 1 || raise(ArgumentError, 'hotkey must have length 1')
@hotkey = arg.to_s.downcase
config[:hotkey] = arg.to_s.downcase
end
def hotkey
config[:hotkey] || 'k'
end

attr_writer :placeholder
def placeholder=(arg)
config[:placeholder] = arg
end
def placeholder
return @placeholder.presence unless @placeholder.nil?
return config[:placeholder].presence unless config[:placeholder].nil?

help_item = items.find { |i| i.type == :help }
help_item && "Hint: Type `#{help_item.title}` for help"
end

private

def config
@config ||= {}
end

def omnibar_class
self.class.name || raise(<<~EOS)
RailsOmnibar subclasses must be assigned to constants
before configuring or rendering them. E.g.:
RailsOmnibar subclasses must be assigned to constants, e.g.:
Foo = Class.new(RailsOmnibar)
Foo.configure { ... }
Foo = Class.new(RailsOmnibar).configure { ... }
EOS
end
end
9 changes: 9 additions & 0 deletions lib/rails_omnibar/inheritance.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class RailsOmnibar
def self.inherited(subclass)
bar1 = singleton
bar2 = subclass.send(:singleton)
%i[@commands @config @items].each do |ivar|
bar2.instance_variable_set(ivar, bar1.instance_variable_get(ivar).dup)
end
end
end
12 changes: 9 additions & 3 deletions lib/rails_omnibar/item/base.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
class RailsOmnibar
module Item
class Base
attr_reader :title, :url, :icon, :modal_html, :suggested, :type
attr_reader :title, :url, :icon, :modal_html, :suggested, :type, :if

def initialize(title:, url: nil, icon: nil, modal_html: nil, suggested: false, type: :default)
def initialize(title:, url: nil, icon: nil, modal_html: nil, suggested: false, type: :default, if: nil)
url.present? && modal_html.present? && raise(ArgumentError, 'use EITHER url: OR modal_html:')

@title = validate_title(title)
Expand All @@ -12,10 +12,16 @@ def initialize(title:, url: nil, icon: nil, modal_html: nil, suggested: false, t
@modal_html = modal_html
@suggested = !!suggested
@type = type
@if = RailsOmnibar.cast_to_condition(binding.local_variable_get(:if))
end

def as_json(*)
{ title: title, url: url, icon: icon, modalHTML: modal_html, suggested: suggested, type: type }
@as_json ||=
{ title: title, url: url, icon: icon, modalHTML: modal_html, suggested: suggested, type: type }
end

def render?(context:, omnibar:)
RailsOmnibar.evaluate_condition(self.if, context: context, omnibar: omnibar)
end

private
Expand Down
5 changes: 2 additions & 3 deletions lib/rails_omnibar/items.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
class RailsOmnibar
def add_item(item)
check_const_and_clear_cache
items << RailsOmnibar.cast_to_item(item)
self
self.class
end

def add_items(*args)
args.each { |arg| add_item(arg) }
self
self.class
end

def self.cast_to_item(arg)
Expand Down
Loading

0 comments on commit b3844f5

Please sign in to comment.