rails_omnibar 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,15 @@
1
1
  class RailsOmnibar
2
2
  def handle(input, controller)
3
- handler = commands.find { |h| h.pattern.match?(input) }
3
+ handler = commands.find do |cmd|
4
+ cmd.handle?(input, controller: controller, omnibar: self)
5
+ end
4
6
  handler&.call(input, controller: controller, omnibar: self) || []
5
7
  end
6
8
 
7
- def command_pattern
8
- commands.any? ? Regexp.union(commands.map(&:pattern)) : /$NO_COMMANDS/
9
- end
10
-
11
9
  def add_command(command)
12
- check_const_and_clear_cache
13
10
  commands << RailsOmnibar.cast_to_command(command)
14
- self
11
+ clear_command_pattern_cache
12
+ self.class
15
13
  end
16
14
 
17
15
  def self.cast_to_command(arg)
@@ -43,6 +41,17 @@ class RailsOmnibar
43
41
 
44
42
  private
45
43
 
44
+ def command_pattern
45
+ @command_pattern ||= begin
46
+ re = commands.any? ? Regexp.union(commands.map(&:pattern)) : /$NO_COMMANDS/
47
+ JsRegex.new!(re, target: 'ES2018')
48
+ end
49
+ end
50
+
51
+ def clear_command_pattern_cache
52
+ @command_pattern = nil
53
+ end
54
+
46
55
  def commands
47
56
  @commands ||= []
48
57
  end
@@ -0,0 +1,27 @@
1
+ class RailsOmnibar
2
+ def self.cast_to_condition(arg)
3
+ case arg
4
+ when nil, true, false then arg
5
+ else
6
+ arg.try(:arity) == 0 ? arg : RailsOmnibar.cast_to_proc(arg)
7
+ end
8
+ end
9
+
10
+ def self.evaluate_condition(condition, context:, omnibar:)
11
+ case condition
12
+ when nil, true then true
13
+ when false then false
14
+ else
15
+ context || raise(<<~EOS)
16
+ Missing context for condition, please render the omnibar with `.render(self)`
17
+ EOS
18
+ if condition.try(:arity) == 0
19
+ context.instance_exec(&condition)
20
+ elsif condition.respond_to?(:call)
21
+ condition.call(context, controller: context, omnibar: omnibar)
22
+ else
23
+ raise("unsupported condition type: #{condition.class}")
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,12 +1,14 @@
1
1
  class RailsOmnibar
2
2
  def configure(&block)
3
- check_const_and_clear_cache
4
3
  tap(&block)
4
+ self.class
5
5
  end
6
6
 
7
- attr_reader :auth
8
7
  def auth=(arg)
9
- @auth = arg.try(:arity) == 0 ? arg : RailsOmnibar.cast_to_proc(arg)
8
+ config[:auth] = arg.try(:arity) == 0 ? arg : RailsOmnibar.cast_to_proc(arg)
9
+ end
10
+ def auth
11
+ config[:auth]
10
12
  end
11
13
  def authorize(controller)
12
14
  if auth.nil?
@@ -20,33 +22,39 @@ class RailsOmnibar
20
22
 
21
23
  def max_results=(arg)
22
24
  arg.is_a?(Integer) && arg > 0 || raise(ArgumentError, 'max_results must be > 0')
23
- @max_results = arg
25
+ config[:max_results] = arg
24
26
  end
25
27
  def max_results
26
- @max_results || 10
28
+ config[:max_results] || 10
27
29
  end
28
30
 
29
- attr_writer :modal
31
+ def modal=(arg)
32
+ config[:modal] = arg
33
+ end
30
34
  def modal?
31
- instance_variable_defined?(:@modal) ? !!@modal : false
35
+ config.key?(:modal) ? !!config[:modal] : false
32
36
  end
33
37
 
34
- attr_writer :calculator
38
+ def calculator=(arg)
39
+ config[:calculator] = arg
40
+ end
35
41
  def calculator?
36
- instance_variable_defined?(:@calculator) ? !!@calculator : true
42
+ config.key?(:calculator) ? !!config[:calculator] : true
37
43
  end
38
44
 
39
- def hotkey
40
- @hotkey || 'k'
41
- end
42
45
  def hotkey=(arg)
43
46
  arg.to_s.size == 1 || raise(ArgumentError, 'hotkey must have length 1')
44
- @hotkey = arg.to_s.downcase
47
+ config[:hotkey] = arg.to_s.downcase
48
+ end
49
+ def hotkey
50
+ config[:hotkey] || 'k'
45
51
  end
46
52
 
47
- attr_writer :placeholder
53
+ def placeholder=(arg)
54
+ config[:placeholder] = arg
55
+ end
48
56
  def placeholder
49
- return @placeholder.presence unless @placeholder.nil?
57
+ return config[:placeholder].presence unless config[:placeholder].nil?
50
58
 
51
59
  help_item = items.find { |i| i.type == :help }
52
60
  help_item && "Hint: Type `#{help_item.title}` for help"
@@ -54,13 +62,15 @@ class RailsOmnibar
54
62
 
55
63
  private
56
64
 
65
+ def config
66
+ @config ||= {}
67
+ end
68
+
57
69
  def omnibar_class
58
70
  self.class.name || raise(<<~EOS)
59
- RailsOmnibar subclasses must be assigned to constants
60
- before configuring or rendering them. E.g.:
71
+ RailsOmnibar subclasses must be assigned to constants, e.g.:
61
72
 
62
- Foo = Class.new(RailsOmnibar)
63
- Foo.configure { ... }
73
+ Foo = Class.new(RailsOmnibar).configure { ... }
64
74
  EOS
65
75
  end
66
76
  end
@@ -0,0 +1,9 @@
1
+ class RailsOmnibar
2
+ def self.inherited(subclass)
3
+ bar1 = singleton
4
+ bar2 = subclass.send(:singleton)
5
+ %i[@commands @config @items].each do |ivar|
6
+ bar2.instance_variable_set(ivar, bar1.instance_variable_get(ivar).dup)
7
+ end
8
+ end
9
+ end
@@ -1,9 +1,9 @@
1
1
  class RailsOmnibar
2
2
  module Item
3
3
  class Base
4
- attr_reader :title, :url, :icon, :modal_html, :suggested, :type
4
+ attr_reader :title, :url, :icon, :modal_html, :suggested, :type, :if
5
5
 
6
- def initialize(title:, url: nil, icon: nil, modal_html: nil, suggested: false, type: :default)
6
+ def initialize(title:, url: nil, icon: nil, modal_html: nil, suggested: false, type: :default, if: nil)
7
7
  url.present? && modal_html.present? && raise(ArgumentError, 'use EITHER url: OR modal_html:')
8
8
 
9
9
  @title = validate_title(title)
@@ -12,10 +12,16 @@ class RailsOmnibar
12
12
  @modal_html = modal_html
13
13
  @suggested = !!suggested
14
14
  @type = type
15
+ @if = RailsOmnibar.cast_to_condition(binding.local_variable_get(:if))
15
16
  end
16
17
 
17
18
  def as_json(*)
18
- { title: title, url: url, icon: icon, modalHTML: modal_html, suggested: suggested, type: type }
19
+ @as_json ||=
20
+ { title: title, url: url, icon: icon, modalHTML: modal_html, suggested: suggested, type: type }
21
+ end
22
+
23
+ def render?(context:, omnibar:)
24
+ RailsOmnibar.evaluate_condition(self.if, context: context, omnibar: omnibar)
19
25
  end
20
26
 
21
27
  private
@@ -1,13 +1,12 @@
1
1
  class RailsOmnibar
2
2
  def add_item(item)
3
- check_const_and_clear_cache
4
3
  items << RailsOmnibar.cast_to_item(item)
5
- self
4
+ self.class
6
5
  end
7
6
 
8
7
  def add_items(*args)
9
8
  args.each { |arg| add_item(arg) }
10
- self
9
+ self.class
11
10
  end
12
11
 
13
12
  def self.cast_to_item(arg)
@@ -1,6 +1,7 @@
1
1
  class RailsOmnibar
2
- def render
3
- @cached_html ||= <<~HTML.html_safe
2
+ def render(context = nil)
3
+ @context = context
4
+ <<~HTML.html_safe
4
5
  <script src='#{urls.js_path}?v=#{RailsOmnibar::VERSION}' type='text/javascript'></script>
5
6
  <div id='mount-rails-omnibar'>
6
7
  <script type="application/json">#{to_json}</script>
@@ -17,9 +18,9 @@ class RailsOmnibar
17
18
  def as_json(*)
18
19
  {
19
20
  calculator: calculator?,
20
- commandPattern: JsRegex.new!(command_pattern, target: 'ES2018'),
21
+ commandPattern: command_pattern,
21
22
  hotkey: hotkey,
22
- items: items,
23
+ items: items.select { |i| i.render?(context: @context, omnibar: self) },
23
24
  maxResults: max_results,
24
25
  modal: modal?,
25
26
  placeholder: placeholder,
@@ -30,11 +31,4 @@ class RailsOmnibar
30
31
  def urls
31
32
  @urls ||= RailsOmnibar::Engine.routes.url_helpers
32
33
  end
33
-
34
- private
35
-
36
- def check_const_and_clear_cache
37
- omnibar_class # trigger constant assignment check
38
- @cached_html = nil
39
- end
40
34
  end
@@ -1,3 +1,3 @@
1
1
  class RailsOmnibar
2
- VERSION = '1.6.0'
2
+ VERSION = '1.8.0'
3
3
  end
data/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "dependencies": {
7
7
  "@heroicons/react": "^2.0.14",
8
8
  "fuzzysort": "^2.0.4",
9
- "omnibar2": "^2.4.5",
9
+ "omnibar2": "^2.5.0",
10
10
  "preact": "^10.11.3",
11
11
  "preact-habitat": "^3.3.0",
12
12
  "react-modal": "^3.16.1",
@@ -0,0 +1,16 @@
1
+ require_relative '../rails_helper'
2
+ require 'bundler/inline'
3
+
4
+ gemfile do
5
+ source 'https://rubygems.org'
6
+ gem 'benchmark-ips', require: 'benchmark/ips'
7
+ end
8
+
9
+ BenchmarkBar = Class.new(RailsOmnibar).configure do |c|
10
+ 100.times { c.add_item(title: rand.to_s, url: rand.to_s) }
11
+ 10.times { c.add_command(description: rand.to_s, pattern: /#{rand}/, example: rand.to_s, resolver: ->(_){}) }
12
+ end
13
+
14
+ Benchmark.ips do |x|
15
+ x.report('render') { BenchmarkBar.render } # ~3k ips
16
+ end
@@ -53,10 +53,8 @@ describe RailsOmnibar do
53
53
  expect(subject.placeholder).to eq nil
54
54
  end
55
55
 
56
- it 'raises when trying to configure or render from an anonymous class' do
56
+ it 'raises when trying to render from an anonymous class' do
57
57
  klass = Class.new(RailsOmnibar)
58
- expect { klass.configure {} }.to raise_error(/constant/)
59
- expect { klass.add_item(title: 'a', url: 'b') }.to raise_error(/constant/)
60
58
  expect { klass.render }.to raise_error(/constant/)
61
59
  end
62
60
  end
@@ -0,0 +1,79 @@
1
+ require 'rails_helper'
2
+
3
+ describe RailsOmnibar do
4
+ it 'inherits commands from a parent class' do
5
+ bar1 = Class.new(RailsOmnibar).configure do |c|
6
+ c.add_command(pattern: /foo1/, resolver: ->(_){})
7
+ end
8
+ bar2 = Class.new(bar1).configure do |c|
9
+ c.add_command(pattern: /foo2/, resolver: ->(_){})
10
+ end
11
+ bar3 = Class.new(bar2).configure do |c|
12
+ c.add_command(pattern: /foo3/, resolver: ->(_){})
13
+ end
14
+
15
+ commands = ->(bar) do
16
+ bar.send(:singleton).send(:commands).map { |c| c.pattern.source }
17
+ end
18
+
19
+ expect(commands.call(bar1)).to eq ['foo1']
20
+ expect(commands.call(bar2)).to eq ['foo1', 'foo2']
21
+ expect(commands.call(bar3)).to eq ['foo1', 'foo2', 'foo3']
22
+
23
+ # later changes to commands should not affect child classes
24
+ bar1.add_command(pattern: /foo1B/, resolver: ->(_){})
25
+ expect(commands.call(bar1)).to eq ['foo1', 'foo1B']
26
+ expect(commands.call(bar2)).to eq ['foo1', 'foo2']
27
+ expect(commands.call(bar3)).to eq ['foo1', 'foo2', 'foo3']
28
+ end
29
+
30
+ it 'inherits configuration from a parent class' do
31
+ bar1 = Class.new(RailsOmnibar).configure do |c|
32
+ c.max_results = 7
33
+ c.placeholder = 'hi'
34
+ c.hotkey = 'j'
35
+ end
36
+ bar2 = Class.new(bar1).configure do |c|
37
+ c.max_results = 8
38
+ end
39
+ bar3 = Class.new(bar2).configure do |c|
40
+ c.hotkey = 'k'
41
+ end
42
+
43
+ config = ->(bar) { bar.send(:singleton).send(:config) }
44
+
45
+ expect(config.call(bar1)).to eq(max_results: 7, placeholder: 'hi', hotkey: 'j')
46
+ expect(config.call(bar2)).to eq(max_results: 8, placeholder: 'hi', hotkey: 'j')
47
+ expect(config.call(bar3)).to eq(max_results: 8, placeholder: 'hi', hotkey: 'k')
48
+
49
+ # later changes to config should not affect child classes
50
+ bar1.placeholder = 'bye'
51
+ expect(bar1.placeholder).to eq 'bye'
52
+ expect(bar2.placeholder).to eq 'hi'
53
+ expect(bar3.placeholder).to eq 'hi'
54
+ end
55
+
56
+ it 'inherits items from a parent class' do
57
+ bar1 = Class.new(RailsOmnibar).configure do |c|
58
+ c.add_item(title: 'foo1', url: 'bar1')
59
+ end
60
+ bar2 = Class.new(bar1).configure do |c|
61
+ c.add_item(title: 'foo2', url: 'bar2')
62
+ end
63
+ bar3 = Class.new(bar2).configure do |c|
64
+ c.add_item(title: 'foo3', url: 'bar3')
65
+ end
66
+
67
+ items = ->(bar) { bar.send(:singleton).send(:items).map(&:title) }
68
+
69
+ expect(items.call(bar1)).to eq ['foo1']
70
+ expect(items.call(bar2)).to eq ['foo1', 'foo2']
71
+ expect(items.call(bar3)).to eq ['foo1', 'foo2', 'foo3']
72
+
73
+ # later changes to items should not affect child classes
74
+ bar1.add_item(title: 'foo1B', url: 'bar1B')
75
+ expect(items.call(bar1)).to eq ['foo1', 'foo1B']
76
+ expect(items.call(bar2)).to eq ['foo1', 'foo2']
77
+ expect(items.call(bar3)).to eq ['foo1', 'foo2', 'foo3']
78
+ end
79
+ end
@@ -119,6 +119,33 @@ describe RailsOmnibar do
119
119
  expect(page).not_to have_content 'fake_result_1'
120
120
  end
121
121
 
122
+ it 'can have conditional items and commands' do
123
+ visit main_app.root_path
124
+ send_keys([:control, 'm']) # custom hotkey, c.f. my_omnibar_template.rb
125
+ expect(page).to have_selector 'input'
126
+
127
+ type('condi')
128
+ expect(page).not_to have_content 'conditional item'
129
+
130
+ FactoryBot.create(:user)
131
+ expect { type('DELETE users'); sleep 0.3 }.not_to change { User.count }
132
+
133
+ # now again with truthy condition
134
+ ENV['FAKE_OMNIBAR_IF'] = '1'
135
+ refresh # reload page
136
+
137
+ send_keys([:control, 'm']) # custom hotkey, c.f. my_omnibar_template.rb
138
+ expect(page).to have_selector 'input'
139
+
140
+ type('condi')
141
+ expect(page).to have_content 'conditional item'
142
+
143
+ FactoryBot.create(:user)
144
+ expect { type('DELETE users'); sleep 0.3 }.to change { User.count }.to(0)
145
+ ensure
146
+ ENV['FAKE_OMNIBAR_IF'] = nil
147
+ end
148
+
122
149
  def type(str)
123
150
  find('input').set(str)
124
151
  end
@@ -29,7 +29,7 @@ file 'app/lib/aa/users.rb', File.read(__dir__ + '/user_resource_template.rb
29
29
 
30
30
  inject_into_class 'app/controllers/application_controller.rb', 'ApplicationController', <<-RUBY
31
31
  def index
32
- render html: (MyOmnibar.render + OtherOmnibar.render)
32
+ render html: (MyOmnibar.render(self) + OtherOmnibar.render(self))
33
33
  end
34
34
  RUBY
35
35
 
@@ -3,6 +3,7 @@ MyOmnibar = RailsOmnibar.configure do |c|
3
3
 
4
4
  c.add_item(title: 'important URL', url: 'https://www.disney.com', suggested: true)
5
5
  c.add_item(title: 'boring URL', url: 'https://www.github.com')
6
+ c.add_item(title: 'conditional item', url: '#', if: -> { ENV['FAKE_OMNIBAR_IF'] })
6
7
 
7
8
  c.add_webadmin_items(prefix: 'Admin:')
8
9
 
@@ -43,6 +44,13 @@ MyOmnibar = RailsOmnibar.configure do |c|
43
44
  end,
44
45
  )
45
46
 
47
+ c.add_command(
48
+ description: 'Delete users',
49
+ pattern: /DELETE (.+)/i,
50
+ resolver: ->(value){ { title: value.classify.constantize.delete_all.to_s } },
51
+ if: ->{ ENV['FAKE_OMNIBAR_IF'] },
52
+ )
53
+
46
54
  c.add_help
47
55
 
48
56
  # Use a hotkey that is the same in most keyboard layouts to work around
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_omnibar
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janosch Müller
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-01-25 00:00:00.000000000 Z
10
+ date: 2024-07-06 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: js_regex
@@ -45,7 +44,6 @@ dependencies:
45
44
  - !ruby/object:Gem::Version
46
45
  version: '8.0'
47
46
  description: Omnibar for Rails
48
- email:
49
47
  executables: []
50
48
  extensions: []
51
49
  extra_rdoc_files: []
@@ -67,6 +65,7 @@ files:
67
65
  - javascript/compiled.js
68
66
  - javascript/src/app.tsx
69
67
  - javascript/src/hooks/index.ts
68
+ - javascript/src/hooks/use_delayed_loading_style.tsx
70
69
  - javascript/src/hooks/use_hotkey.ts
71
70
  - javascript/src/hooks/use_item_action.tsx
72
71
  - javascript/src/hooks/use_modal.tsx
@@ -79,8 +78,10 @@ files:
79
78
  - lib/rails_omnibar/command/base.rb
80
79
  - lib/rails_omnibar/command/search.rb
81
80
  - lib/rails_omnibar/commands.rb
81
+ - lib/rails_omnibar/conditions.rb
82
82
  - lib/rails_omnibar/config.rb
83
83
  - lib/rails_omnibar/engine.rb
84
+ - lib/rails_omnibar/inheritance.rb
84
85
  - lib/rails_omnibar/item/base.rb
85
86
  - lib/rails_omnibar/item/help.rb
86
87
  - lib/rails_omnibar/item/webadmin.rb
@@ -89,7 +90,9 @@ files:
89
90
  - lib/rails_omnibar/version.rb
90
91
  - package.json
91
92
  - rails_omnibar.gemspec
93
+ - spec/benchmarks/benchmark.rb
92
94
  - spec/lib/rails_omnibar/config_spec.rb
95
+ - spec/lib/rails_omnibar/inheritance_spec.rb
93
96
  - spec/lib/rails_omnibar/item/base_spec.rb
94
97
  - spec/lib/rails_omnibar/version_spec.rb
95
98
  - spec/rails_helper.rb
@@ -105,7 +108,6 @@ homepage: https://github.com/jaynetics/rails_omnibar
105
108
  licenses:
106
109
  - MIT
107
110
  metadata: {}
108
- post_install_message:
109
111
  rdoc_options: []
110
112
  require_paths:
111
113
  - lib
@@ -120,12 +122,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
120
122
  - !ruby/object:Gem::Version
121
123
  version: '0'
122
124
  requirements: []
123
- rubygems_version: 3.5.0.dev
124
- signing_key:
125
+ rubygems_version: 3.6.0.dev
125
126
  specification_version: 4
126
127
  summary: Omnibar for Rails
127
128
  test_files:
129
+ - spec/benchmarks/benchmark.rb
128
130
  - spec/lib/rails_omnibar/config_spec.rb
131
+ - spec/lib/rails_omnibar/inheritance_spec.rb
129
132
  - spec/lib/rails_omnibar/item/base_spec.rb
130
133
  - spec/lib/rails_omnibar/version_spec.rb
131
134
  - spec/rails_helper.rb