rails_omnibar 1.6.0 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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