power-compass 0.1.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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +13 -0
- data/app/controllers/compass/application_controller.rb +37 -0
- data/app/controllers/compass/menu_controller.rb +31 -0
- data/app/controllers/compass/notification_controller.rb +15 -0
- data/app/controllers/compass/search_controller.rb +62 -0
- data/config/routes.rb +12 -0
- data/lib/compass/breadcrumb/middleware.rb +44 -0
- data/lib/compass/breadcrumb.rb +46 -0
- data/lib/compass/configuration/breadcrumb.rb +24 -0
- data/lib/compass/configuration/menu.rb +22 -0
- data/lib/compass/configuration/notification.rb +36 -0
- data/lib/compass/configuration/search.rb +18 -0
- data/lib/compass/configuration.rb +45 -0
- data/lib/compass/engine.rb +27 -0
- data/lib/compass/lazy_attribute.rb +55 -0
- data/lib/compass/menu/item.rb +91 -0
- data/lib/compass/menu/item_list.rb +57 -0
- data/lib/compass/menu.rb +117 -0
- data/lib/compass/notification/provider.rb +73 -0
- data/lib/compass/notification.rb +5 -0
- data/lib/compass/rspec/menu.rb +108 -0
- data/lib/compass/rspec.rb +5 -0
- data/lib/compass/search/provider.rb +118 -0
- data/lib/compass/search/rendering.rb +27 -0
- data/lib/compass/search/view_context.rb +22 -0
- data/lib/compass/search.rb +7 -0
- data/lib/compass/version.rb +3 -0
- data/lib/compass.rb +19 -0
- data/lib/tasks/compass_tasks.rake +4 -0
- metadata +97 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 62d24dd9988012b6ce11695c503b785d1f0fc662c08708fefad59eaab4d8a96b
|
|
4
|
+
data.tar.gz: f04fbc2588e8013529171751bf5df84026d5743ef605dc74f7d3cffd974c7d50
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 10be0899560a5fd97c5f14ffcad8208e535cd1b597ef058804806f4b4cc4b0b050126e6e1d108bc304708aaae536d4f19d4c2661a9b65e9a17d960232d1dfa56
|
|
7
|
+
data.tar.gz: 61b19fa23127e9e6a6c9efff137692ce56ddb95476928bd8136b0b034730a4808a2c9dc8c6683a8a70d79093d152733898ed157cbf39b4207c980aa290112c13
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright Carlos Palhares
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Compass
|
|
2
|
+
Short description and motivation.
|
|
3
|
+
|
|
4
|
+
## Usage
|
|
5
|
+
How to use my plugin.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
Add this line to your application's Gemfile:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
gem "compass"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
And then execute:
|
|
15
|
+
```bash
|
|
16
|
+
$ bundle
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install it yourself as:
|
|
20
|
+
```bash
|
|
21
|
+
$ gem install compass
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Contributing
|
|
25
|
+
Contribution directions go here.
|
|
26
|
+
|
|
27
|
+
## License
|
|
28
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "bundler/gem_tasks"
|
|
5
|
+
load "rails/tasks/statistics.rake"
|
|
6
|
+
|
|
7
|
+
require "rspec/core/rake_task"
|
|
8
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
9
|
+
|
|
10
|
+
require "rubocop/rake_task"
|
|
11
|
+
RuboCop::RakeTask.new(:rubocop)
|
|
12
|
+
|
|
13
|
+
task default: %i[rubocop spec]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Compass
|
|
2
|
+
# Base compass controller.
|
|
3
|
+
#
|
|
4
|
+
# @private
|
|
5
|
+
class ApplicationController < ActionController::API
|
|
6
|
+
etag { current_context_id }
|
|
7
|
+
etag { context_modified_at }
|
|
8
|
+
|
|
9
|
+
before_action unless: :authenticate do
|
|
10
|
+
head(:forbidden)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
before_action unless: :validate_context_id do
|
|
14
|
+
head(:bad_request)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def authenticate
|
|
18
|
+
instance_exec(&Compass.config.authenticate)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def current_context
|
|
22
|
+
instance_exec(&Compass.config.context)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def current_context_id
|
|
26
|
+
instance_exec(&Compass.config.context_id)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def context_modified_at
|
|
30
|
+
instance_exec(&Compass.config.modified_at)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def validate_context_id
|
|
34
|
+
current_context_id.to_s.eql?(params[:context_id])
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Compass
|
|
2
|
+
# Base compass controller.
|
|
3
|
+
#
|
|
4
|
+
# @private
|
|
5
|
+
class MenuController < ApplicationController
|
|
6
|
+
etag { filter.values.compact }
|
|
7
|
+
etag { menu_items.maximum(:updated_at) }
|
|
8
|
+
|
|
9
|
+
def index
|
|
10
|
+
expires_in(*Array(Compass.config.menu.cache))
|
|
11
|
+
|
|
12
|
+
return unless stale? menu_items, etag: "compass-menu"
|
|
13
|
+
|
|
14
|
+
render json: menu_items, filter: filter
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def menu_items
|
|
20
|
+
@menu_items ||= Compass::Menu.build(**current_context)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def filter
|
|
24
|
+
{
|
|
25
|
+
authorized: true,
|
|
26
|
+
tagged: params[:tagged],
|
|
27
|
+
sort: params.fetch(:sort, %i[ gravity label ])
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require "compass/notification/provider"
|
|
2
|
+
|
|
3
|
+
module Compass
|
|
4
|
+
class NotificationController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
expires_in(*Array(Compass.config.notification.cache), public: true)
|
|
7
|
+
|
|
8
|
+
providers = Compass::Notification::Provider.global_notifications(current_context)
|
|
9
|
+
|
|
10
|
+
return unless stale?(providers, etag: "compass-notification")
|
|
11
|
+
|
|
12
|
+
render json: providers
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
require "compass/search/provider"
|
|
2
|
+
|
|
3
|
+
module Compass
|
|
4
|
+
class SearchController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
if params[:q]
|
|
7
|
+
results = Compass::Search::Provider.global_search(params.require(:q), current_context)
|
|
8
|
+
render json: { results: results }
|
|
9
|
+
else
|
|
10
|
+
providers
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def show
|
|
15
|
+
result = Compass::Search::Provider.search(params[:q], params[:provider_name], current_context)
|
|
16
|
+
render json: {
|
|
17
|
+
provider: params[:provider_name],
|
|
18
|
+
context_id: params[:context_id],
|
|
19
|
+
**result
|
|
20
|
+
}
|
|
21
|
+
rescue Compass::Search::UnknownProvider => error
|
|
22
|
+
render json: {
|
|
23
|
+
error: error.message,
|
|
24
|
+
available_providers: error.available_providers
|
|
25
|
+
}, status: :not_found
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def providers
|
|
29
|
+
render json: {
|
|
30
|
+
context_id: params[:context_id],
|
|
31
|
+
providers: available_provider_names.map do |name|
|
|
32
|
+
provider_class = find_provider_by_name(name)
|
|
33
|
+
|
|
34
|
+
base_info = {
|
|
35
|
+
name: name,
|
|
36
|
+
class_name: provider_class&.name
|
|
37
|
+
}
|
|
38
|
+
begin
|
|
39
|
+
label = provider_class&.new(**current_context)&.label || name.humanize
|
|
40
|
+
base_info.merge(label: label)
|
|
41
|
+
rescue => e
|
|
42
|
+
Rails.logger.warn "Error getting provider info for #{name}: #{e.message}"
|
|
43
|
+
base_info.merge(
|
|
44
|
+
label: name.humanize,
|
|
45
|
+
error: e.message
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def find_provider_by_name(name)
|
|
55
|
+
Compass::Search::Provider.find_by_name(name)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def available_provider_names
|
|
59
|
+
Compass::Search::Provider.available_provider_names
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Compass::Engine.routes.draw do
|
|
2
|
+
get ":context_id/menu", to: "menu#index", as: :menu
|
|
3
|
+
|
|
4
|
+
get ":context_id/search", to: "search#index", as: :search
|
|
5
|
+
get ":context_id/search/providers", to: "search#providers", as: :search_providers
|
|
6
|
+
get ":context_id/search/:provider_name",
|
|
7
|
+
to: "search#show",
|
|
8
|
+
as: :search_provider,
|
|
9
|
+
constraints: { provider_name: /[a-zA-Z_]+/ }
|
|
10
|
+
|
|
11
|
+
get ":context_id/notifications", to: "notification#index", as: :notifications
|
|
12
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Compass
|
|
2
|
+
# Compass breadcrumb handling module.
|
|
3
|
+
# It leverages the configured breadcrumb strategy to fetch and push breadcrumb items.
|
|
4
|
+
#
|
|
5
|
+
module Breadcrumb
|
|
6
|
+
# Middleware to track breadcrumb items based on requests.
|
|
7
|
+
# It uses the configured breadcrumb strategy to save breadcrumb items
|
|
8
|
+
# for requests that should be tracked.
|
|
9
|
+
#
|
|
10
|
+
class Middleware
|
|
11
|
+
def initialize(app)
|
|
12
|
+
@app = app
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(env)
|
|
16
|
+
@request = ActionDispatch::Request.new(env)
|
|
17
|
+
context = instance_exec(&Compass.config.context)
|
|
18
|
+
|
|
19
|
+
save_breadcrumb!(context) if should_track?(context)
|
|
20
|
+
rescue => e
|
|
21
|
+
# Silently handle breadcrumb errors - don't break the request
|
|
22
|
+
Compass.logger&.error "Breadcrumb middleware error: #{e.message}"
|
|
23
|
+
ensure
|
|
24
|
+
return @app.call(env)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
attr_reader :request
|
|
30
|
+
|
|
31
|
+
def save_breadcrumb!(context)
|
|
32
|
+
::Compass::Breadcrumb.push(
|
|
33
|
+
context: context,
|
|
34
|
+
label: @request.params[:mt],
|
|
35
|
+
url: @request.url
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def should_track?(context)
|
|
40
|
+
Compass.config.breadcrumb.should_track.call(@request, context)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Compass
|
|
2
|
+
# Compass breadcrumb handling module.
|
|
3
|
+
# It leverages the configured breadcrumb strategy to fetch and push breadcrumb items.
|
|
4
|
+
#
|
|
5
|
+
module Breadcrumb
|
|
6
|
+
autoload :Middleware, "compass/breadcrumb/middleware"
|
|
7
|
+
|
|
8
|
+
# Fetch breadcrumb for the given context, using the configured strategy.
|
|
9
|
+
#
|
|
10
|
+
# I.e.:
|
|
11
|
+
#
|
|
12
|
+
# Compass::Breadcrumb.for(context_id, context: { current_user: }, max: 5)
|
|
13
|
+
# => [
|
|
14
|
+
# { url: "/home", label: "Home" },
|
|
15
|
+
# { url: "/section", label: "Section" },
|
|
16
|
+
# { url: "/section/page", label: "Page" }
|
|
17
|
+
# ]
|
|
18
|
+
#
|
|
19
|
+
# @param context_id [String] The context identifier.
|
|
20
|
+
# @param context [Hash] The context hash.
|
|
21
|
+
# @param max [Integer] The maximum number of items to return.
|
|
22
|
+
# @return [Array<Hash>,nil] The breadcrumb items.
|
|
23
|
+
def self.for(context: {}, max: 10)
|
|
24
|
+
Compass.config.breadcrumb.strategy&.for(context: context, max: max)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Push a new breadcrumb item for the given context, using the configured strategy.
|
|
28
|
+
#
|
|
29
|
+
# I.e.:
|
|
30
|
+
#
|
|
31
|
+
# Compass::Breadcrumb.push(
|
|
32
|
+
# context_id,
|
|
33
|
+
# context: { current_user: },
|
|
34
|
+
# url: "/section/page",
|
|
35
|
+
# label: "Page"
|
|
36
|
+
# )
|
|
37
|
+
#
|
|
38
|
+
# @param context_id [String] The context identifier.
|
|
39
|
+
# @param context [Hash] The context hash.
|
|
40
|
+
# @param url [String] The URL of the breadcrumb item.
|
|
41
|
+
# @param label [String] The label of the breadcrumb item.
|
|
42
|
+
def self.push(context: {}, url:, label:)
|
|
43
|
+
Compass.config.breadcrumb.strategy&.push(context: context, url: url, label: label)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Compass
|
|
2
|
+
module Configuration::Breadcrumb
|
|
3
|
+
include ActiveSupport::Configurable
|
|
4
|
+
|
|
5
|
+
# Strategy class for breadcrumb functionality.
|
|
6
|
+
# Applications must provide their own implementation.
|
|
7
|
+
#
|
|
8
|
+
# I.e.:
|
|
9
|
+
# config.breadcrumb.strategy = MyAppBreadcrumbService
|
|
10
|
+
#
|
|
11
|
+
# @see Compass::Breadcrumb for method signatures.
|
|
12
|
+
#
|
|
13
|
+
config_accessor :strategy, default: nil
|
|
14
|
+
|
|
15
|
+
# Proc to determine if a request should be tracked for breadcrumbs.
|
|
16
|
+
# It receives the request and context as parameters.
|
|
17
|
+
#
|
|
18
|
+
# I.e.:
|
|
19
|
+
# config.breadcrumb.should_track = ->(request, context) { request.path != "/login" }
|
|
20
|
+
#
|
|
21
|
+
# @return [Proc] The tracking determination proc.
|
|
22
|
+
config_accessor :should_track, default: ->(request, context) { true }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Compass
|
|
2
|
+
# Compass configuration for menu items.
|
|
3
|
+
module Configuration::Menu
|
|
4
|
+
include ActiveSupport::Configurable
|
|
5
|
+
|
|
6
|
+
# List of menu classes to be autoloaded.
|
|
7
|
+
#
|
|
8
|
+
# I.e.: Compass.config.menu.items = %w[ ProjectMenu UserMenu ]
|
|
9
|
+
config_accessor :items, default: []
|
|
10
|
+
|
|
11
|
+
# Cache-Control configuration for menu items.
|
|
12
|
+
# This configuration is used to set the `Cache-Control` header in the response.
|
|
13
|
+
# You can set the cache max age in seconds, and other Cache-Control attributes.
|
|
14
|
+
# The first argument is always the cache max age, either in seconds or in
|
|
15
|
+
# ActiveSupport::Duration.
|
|
16
|
+
#
|
|
17
|
+
# I.e.: Compass.config.menu.cache = 3600
|
|
18
|
+
# I.e.: Compass.config.menu.cache = [1.hour, public: false]
|
|
19
|
+
#
|
|
20
|
+
config_accessor :cache, default: 3600
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Compass
|
|
2
|
+
module Configuration
|
|
3
|
+
# Compass configuration for notifications.
|
|
4
|
+
class Notification
|
|
5
|
+
include ActiveSupport::Configurable
|
|
6
|
+
|
|
7
|
+
# List of notification provider classes to be used.
|
|
8
|
+
#
|
|
9
|
+
# Example:
|
|
10
|
+
# Compass.config.notification.providers = [UserNotificationProvider, ProjectNotificationProvider]
|
|
11
|
+
config_accessor :providers, default: []
|
|
12
|
+
|
|
13
|
+
# Polling interval for notifications (in seconds).
|
|
14
|
+
# Can be set to an integer or a lambda returning an integer.
|
|
15
|
+
#
|
|
16
|
+
# Example:
|
|
17
|
+
# Compass.config.notification.polling_interval = 30
|
|
18
|
+
config_accessor :polling_interval, default: -> { 30 }
|
|
19
|
+
|
|
20
|
+
# Maximum number of notifications to return.
|
|
21
|
+
#
|
|
22
|
+
# Example:
|
|
23
|
+
# Compass.config.notification.max_notifications = 100
|
|
24
|
+
config_accessor :max_notifications, default: 100
|
|
25
|
+
|
|
26
|
+
# Cache-Control configuration for notifications.
|
|
27
|
+
# Used to set the `Cache-Control` header in the response.
|
|
28
|
+
# The first argument is the cache max age (in seconds or ActiveSupport::Duration).
|
|
29
|
+
#
|
|
30
|
+
# Example:
|
|
31
|
+
# Compass.config.notification.cache = 3600
|
|
32
|
+
# Compass.config.notification.cache = [1.hour, public: true]
|
|
33
|
+
config_accessor :cache, default: 3600
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Compass
|
|
2
|
+
module Configuration::Search
|
|
3
|
+
# Compass configuration for search providers and view paths.
|
|
4
|
+
include ActiveSupport::Configurable
|
|
5
|
+
|
|
6
|
+
# List of search provider classes to be used.
|
|
7
|
+
#
|
|
8
|
+
# Example:
|
|
9
|
+
# Compass.config.search.providers = [CarsSearchProvider, UserSearchProvider]
|
|
10
|
+
config_accessor :providers, default: []
|
|
11
|
+
|
|
12
|
+
# List of view paths for search rendering.
|
|
13
|
+
#
|
|
14
|
+
# Example:
|
|
15
|
+
# Compass.config.search.view_paths = ["app/views/search"]
|
|
16
|
+
config_accessor :view_paths, default: []
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require "active_support/configurable"
|
|
2
|
+
|
|
3
|
+
# Compass configuration
|
|
4
|
+
module Compass
|
|
5
|
+
# Configuration options for Compass.
|
|
6
|
+
#
|
|
7
|
+
# To configure, use the `configure` method. For example:
|
|
8
|
+
#
|
|
9
|
+
# Compass.configure do |config|
|
|
10
|
+
# config.authenticate = -> do
|
|
11
|
+
# authenticate_or_request_with_http_token do |token, options|
|
|
12
|
+
# @current_user = User.find_by(token: token)
|
|
13
|
+
# end
|
|
14
|
+
# end
|
|
15
|
+
# config.context = -> { { current_user: } }
|
|
16
|
+
# config.etag = -> { current_user.id }
|
|
17
|
+
#
|
|
18
|
+
# # Configure menu
|
|
19
|
+
# config.menu.items = %w[ TestMenu ]
|
|
20
|
+
# config.menu.cache = 3600
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
module Configuration
|
|
24
|
+
extend ::ActiveSupport::Concern
|
|
25
|
+
|
|
26
|
+
autoload :Menu, "compass/configuration/menu"
|
|
27
|
+
autoload :Search, "compass/configuration/search"
|
|
28
|
+
autoload :Notification, "compass/configuration/notification"
|
|
29
|
+
autoload :Breadcrumb, "compass/configuration/breadcrumb"
|
|
30
|
+
|
|
31
|
+
included do
|
|
32
|
+
include ActiveSupport::Configurable
|
|
33
|
+
|
|
34
|
+
config_accessor :authenticate, default: -> { false }
|
|
35
|
+
config_accessor :context, default: -> { {} }
|
|
36
|
+
config_accessor :context_id, default: -> { }
|
|
37
|
+
config_accessor :modified_at, default: -> { }
|
|
38
|
+
|
|
39
|
+
config_accessor :menu, default: Compass::Configuration::Menu
|
|
40
|
+
config_accessor :search, default: Compass::Configuration::Search
|
|
41
|
+
config_accessor :notification, default: Compass::Configuration::Notification
|
|
42
|
+
config_accessor :breadcrumb, default: Compass::Configuration::Breadcrumb
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require "compass"
|
|
2
|
+
|
|
3
|
+
module Compass
|
|
4
|
+
# Rails plugin to autoload compass features such as Menu.
|
|
5
|
+
# Mounting this engine exposes the routes necessary to allow other apps
|
|
6
|
+
# to integrate with this Rails application. See more in `config/routes.rb`.
|
|
7
|
+
#
|
|
8
|
+
class Engine < ::Rails::Engine
|
|
9
|
+
isolate_namespace Compass
|
|
10
|
+
config.generators.api_only = true
|
|
11
|
+
config.compass = Compass.config
|
|
12
|
+
|
|
13
|
+
initializer "compass.logger" do
|
|
14
|
+
Compass.logger ||= Rails.logger.respond_to?(:tagged) ? Rails.logger.tagged("Compass") : Rails.logger
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
initializer "compass.search.view_paths", after: "action_controller.set_configs" do |app|
|
|
18
|
+
ActiveSupport.on_load(:action_controller) do
|
|
19
|
+
Compass.config.search.view_paths = ActionController::Base.view_paths
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
initializer "compass.middleware" do |app|
|
|
24
|
+
app.middleware.use Compass::Breadcrumb::Middleware
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module Compass
|
|
2
|
+
# Allows a Ruby class to define an attribute, that relies on another
|
|
3
|
+
# attribute to evaluate a value.
|
|
4
|
+
# @private
|
|
5
|
+
module LazyAttribute
|
|
6
|
+
# A lazy evaluated value, evaluated in the given context when required.
|
|
7
|
+
#
|
|
8
|
+
# @private
|
|
9
|
+
class LazyValue
|
|
10
|
+
def initialize(ctx, value)
|
|
11
|
+
@ctx = ctx
|
|
12
|
+
@value = value
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def get
|
|
16
|
+
case @value
|
|
17
|
+
when Symbol then @ctx.send(@value)
|
|
18
|
+
when Proc then @ctx.instance_exec(&@value)
|
|
19
|
+
else @value
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
extend ActiveSupport::Concern
|
|
25
|
+
|
|
26
|
+
class_methods do
|
|
27
|
+
# Defines a lazy_attribute, which can be set to a block or a symbol to be
|
|
28
|
+
# lazily evaluated within the context of the given attribute.
|
|
29
|
+
#
|
|
30
|
+
# I.e.:
|
|
31
|
+
#
|
|
32
|
+
# class Car < Struct.new(:engine)
|
|
33
|
+
# lazy_attribute :power, :engine
|
|
34
|
+
# end
|
|
35
|
+
# StrongEngine = Struct.new(:cylinder)
|
|
36
|
+
#
|
|
37
|
+
# car = Car.new(StrongEngine.new(2000))
|
|
38
|
+
# car.power(:cylinder)
|
|
39
|
+
# car.power => 2000
|
|
40
|
+
#
|
|
41
|
+
def lazy_attribute(name, ctxmth, &getter)
|
|
42
|
+
define_method(name) do |value = nil, &block|
|
|
43
|
+
context = send(ctxmth)
|
|
44
|
+
if value.present? || block.present?
|
|
45
|
+
instance_variable_set("@#{name}", LazyValue.new(context, value || block))
|
|
46
|
+
else
|
|
47
|
+
lazy_value = instance_variable_get("@#{name}")
|
|
48
|
+
value = lazy_value ? lazy_value.get : context.respond_to?(name) ? context.send(name) : nil
|
|
49
|
+
instance_exec(value, &(getter || :itself))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
module Compass
|
|
2
|
+
# It is an item builder class. It leverages Compass::LazyAttribute to be able
|
|
3
|
+
# to build its attributes based on a menu context, allowing shared methods to
|
|
4
|
+
# live in the Menu class.
|
|
5
|
+
#
|
|
6
|
+
class Menu::Item
|
|
7
|
+
attr_reader :items, :menu
|
|
8
|
+
|
|
9
|
+
include LazyAttribute
|
|
10
|
+
|
|
11
|
+
def initialize(menu, &block)
|
|
12
|
+
@menu = menu
|
|
13
|
+
@items = Menu::ItemList.new
|
|
14
|
+
|
|
15
|
+
instance_exec(menu, &block)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Item gravity. This is used to sort items in the menu.
|
|
19
|
+
# The default gravity is 1, so if an item should always float to the top,
|
|
20
|
+
# the gravity should be set to 0.#
|
|
21
|
+
lazy_attribute(:gravity, :menu) do |gravity|
|
|
22
|
+
gravity.presence || 1
|
|
23
|
+
end
|
|
24
|
+
# Shallowness of the item. If the item is shallow, it will not be rendered,
|
|
25
|
+
# meaning that its children will be rendered in its place.
|
|
26
|
+
lazy_attribute :shallow, :menu
|
|
27
|
+
# Item label. This is the text that will be displayed in the menu.
|
|
28
|
+
lazy_attribute :label, :menu
|
|
29
|
+
# Item URL. This is the URL that the item will link to.
|
|
30
|
+
lazy_attribute :url, :menu
|
|
31
|
+
# Item icon. This is the icon that will be displayed in the menu.
|
|
32
|
+
lazy_attribute :icon, :menu
|
|
33
|
+
# Item meta data is used for various things, usually tied to the UI behavior.
|
|
34
|
+
# For example, to tell the UI that this item should be rendered in full screen
|
|
35
|
+
# meta could be set to `meta { full_screen true }`.
|
|
36
|
+
lazy_attribute :meta, :menu
|
|
37
|
+
# Badge count. This is the number that will be displayed in the badge when above 0.
|
|
38
|
+
lazy_attribute :badge_count, :menu do |count|
|
|
39
|
+
count.to_i + items.sum(&:badge_count)
|
|
40
|
+
end
|
|
41
|
+
# Authorizes the current context to render the item.
|
|
42
|
+
# I.e.: authorized { current_user.admin? }
|
|
43
|
+
lazy_attribute :authorized, :menu do |authorized|
|
|
44
|
+
authorized || authorized.nil?
|
|
45
|
+
end
|
|
46
|
+
# The last time anything about this item in this context was changed, including the authorization.
|
|
47
|
+
lazy_attribute :updated_at, :menu do |updated_at|
|
|
48
|
+
[ self.items.maximum(:updated_at), updated_at ].compact.max
|
|
49
|
+
end
|
|
50
|
+
# Tags that are associated with the item. This is used to filter items.
|
|
51
|
+
lazy_attribute :tags, :menu do |tags|
|
|
52
|
+
[ *self.items.flat_map(&:tags), *tags ]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Nest items under the current item.
|
|
56
|
+
#
|
|
57
|
+
# I.e.:
|
|
58
|
+
# item do
|
|
59
|
+
# label "Users"
|
|
60
|
+
# item do
|
|
61
|
+
# label "Profile"
|
|
62
|
+
# url :profile_url
|
|
63
|
+
# end
|
|
64
|
+
# end
|
|
65
|
+
#
|
|
66
|
+
def item(&block)
|
|
67
|
+
Menu::Item.new(@menu, &block).tap do |item|
|
|
68
|
+
self.items.push(item)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @private
|
|
73
|
+
def each(&block)
|
|
74
|
+
return yield self unless shallow
|
|
75
|
+
|
|
76
|
+
items.each(&block)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def as_json(options = {})
|
|
80
|
+
{
|
|
81
|
+
label: label,
|
|
82
|
+
icon: icon,
|
|
83
|
+
url: url,
|
|
84
|
+
items: items,
|
|
85
|
+
badge: badge_count.nonzero?,
|
|
86
|
+
meta: meta,
|
|
87
|
+
gravity: gravity
|
|
88
|
+
}.as_json(options).compact_blank
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Compass
|
|
2
|
+
# It is a list of Compass::Menu::Item, which can be filtered and
|
|
3
|
+
# represented as_json recursivelly throuhout the entire tree.
|
|
4
|
+
#
|
|
5
|
+
# @private
|
|
6
|
+
class Menu::ItemList
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
def initialize(items = [])
|
|
10
|
+
@items = items
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def push(...)
|
|
14
|
+
@items.push(...)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def each(&block)
|
|
18
|
+
@items.each do |item|
|
|
19
|
+
item.each(&block)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def as_json(options = {})
|
|
24
|
+
filter(**options.fetch(:filter, {})).to_a.as_json(options)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def filter(**filters)
|
|
28
|
+
filters.reduce(self) do |items, (filter, arg)|
|
|
29
|
+
arg ? items.public_send(filter, arg) : items
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def maximum(attr)
|
|
34
|
+
filter_map(&attr).max
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def select(&block)
|
|
38
|
+
self.class.new(super(&block))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def authorized(*)
|
|
42
|
+
select(&:authorized)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def tagged(tag)
|
|
46
|
+
select { |item| item.tags&.include?(tag) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def sort(attrs)
|
|
50
|
+
attrs = Array(attrs)
|
|
51
|
+
sorted = sort_by do |item|
|
|
52
|
+
attrs.map { |attr| item.public_send(attr) }
|
|
53
|
+
end
|
|
54
|
+
self.class.new(sorted)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/compass/menu.rb
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
module Compass
|
|
2
|
+
# Compass::Menu is the base class for exposing menus from a Compass enabled application.
|
|
3
|
+
# This class acts as a builder for the menu items. Each root menu should extend this
|
|
4
|
+
# class to build a menu tree.
|
|
5
|
+
#
|
|
6
|
+
# I.e.:
|
|
7
|
+
#
|
|
8
|
+
# class UserMenu < Compass::Menu
|
|
9
|
+
# include Rails.application.routes.mounted_helpers
|
|
10
|
+
#
|
|
11
|
+
# item do
|
|
12
|
+
# label "Users"
|
|
13
|
+
#
|
|
14
|
+
# item do
|
|
15
|
+
# label "Profile"
|
|
16
|
+
# url :profile_url
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# item do
|
|
20
|
+
# label "Settings"
|
|
21
|
+
# url :user_settings_url
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# In most cases an application will expose multiple menus, and so an Application level
|
|
27
|
+
# base class should be created:
|
|
28
|
+
#
|
|
29
|
+
# I.e.:
|
|
30
|
+
#
|
|
31
|
+
# class ApplicationMenu < Compass::Menu
|
|
32
|
+
# include Rails.application.routes.mounted_helpers
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# class UserMenu < ApplicationMenu
|
|
36
|
+
# item do
|
|
37
|
+
# label "Users"
|
|
38
|
+
#
|
|
39
|
+
# item do
|
|
40
|
+
# label "Profile"
|
|
41
|
+
# url :profile_url
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# item do
|
|
45
|
+
# label "Settings"
|
|
46
|
+
# url :user_settings_url
|
|
47
|
+
# end
|
|
48
|
+
# end
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
# @see Compass::Menu::Item
|
|
52
|
+
class Menu
|
|
53
|
+
autoload :Item, "compass/menu/item"
|
|
54
|
+
autoload :ItemList, "compass/menu/item_list"
|
|
55
|
+
|
|
56
|
+
# Allow building a menu from a list of items.
|
|
57
|
+
# This is useful for building a menu from a list of items
|
|
58
|
+
# with the given context.
|
|
59
|
+
#
|
|
60
|
+
# I.e.:
|
|
61
|
+
# Compass::Menu.build(["UserMenu", "AdminMenu"], current_user: user)
|
|
62
|
+
#
|
|
63
|
+
# By default, it will build items from `Compass.menus`.
|
|
64
|
+
#
|
|
65
|
+
# I.e.:
|
|
66
|
+
# Compass.config.menu.items = ["UserMenu", "AdminMenu"]
|
|
67
|
+
# Compass::Menu.build(current_user: user)
|
|
68
|
+
#
|
|
69
|
+
# @param items [Array<String>] the list of menu items to build
|
|
70
|
+
# @param context [Hash] the context to pass to the menu items
|
|
71
|
+
# @return [ItemList] the list of menu items
|
|
72
|
+
def self.build(items = Compass.config.menu.items, **context)
|
|
73
|
+
Array(items).map(&:constantize)
|
|
74
|
+
.map { _1.new(**context).build }
|
|
75
|
+
.then(&ItemList.method(:new))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Define the root item of the menu.
|
|
79
|
+
# This method should be called in the class definition to define the root item
|
|
80
|
+
# of the menu.
|
|
81
|
+
#
|
|
82
|
+
# I.e.:
|
|
83
|
+
# class UserMenu < Compass::Menu
|
|
84
|
+
# item do
|
|
85
|
+
# label "Users"
|
|
86
|
+
#
|
|
87
|
+
# item do
|
|
88
|
+
# label "Profile"
|
|
89
|
+
# url :profile_url
|
|
90
|
+
# end
|
|
91
|
+
# end
|
|
92
|
+
# end
|
|
93
|
+
#
|
|
94
|
+
# @yield the block to define the root item
|
|
95
|
+
def self.item(&block)
|
|
96
|
+
define_method(:build) do
|
|
97
|
+
@item ||= Item.new(self, &block)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Initialize the menu with the given context.
|
|
102
|
+
# Each context key will be available as a method on the instance.
|
|
103
|
+
# Because items are built from the context, they will have access
|
|
104
|
+
# to these context methods
|
|
105
|
+
#
|
|
106
|
+
# I.e.:
|
|
107
|
+
# menu = UserMenu.new(current_user: user)
|
|
108
|
+
# menu.current_user # => user
|
|
109
|
+
#
|
|
110
|
+
# @param context [Hash] the context to pass to the menu items
|
|
111
|
+
def initialize(**context)
|
|
112
|
+
context.each do |key, value|
|
|
113
|
+
define_singleton_method(key) { value }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module Compass
|
|
2
|
+
module Notification
|
|
3
|
+
class UnknownProvider < StandardError
|
|
4
|
+
attr_reader :provider_name, :available_providers
|
|
5
|
+
|
|
6
|
+
def initialize(provider_name, available_providers)
|
|
7
|
+
@provider_name = provider_name
|
|
8
|
+
@available_providers = available_providers
|
|
9
|
+
super("Notification provider '#{provider_name}' not found")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class Provider
|
|
14
|
+
class << self
|
|
15
|
+
def global_notifications(context = {})
|
|
16
|
+
Compass.config.notification.providers.map do |provider|
|
|
17
|
+
begin
|
|
18
|
+
provider_class = provider.try(:constantize) || provider
|
|
19
|
+
provider_class.new(**context)
|
|
20
|
+
rescue => e
|
|
21
|
+
Compass.logger&.error("[ Compass::Notification::Provider ] #{provider}: #{e.class}: #{e.message}")
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Instance methods
|
|
29
|
+
def initialize(**context)
|
|
30
|
+
@context = context
|
|
31
|
+
context.each { |key, value| define_singleton_method(key) { value } }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def fetch_notifications
|
|
35
|
+
retrieve_notifications
|
|
36
|
+
rescue => e
|
|
37
|
+
log_error(e)
|
|
38
|
+
[]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def retrieve_notifications
|
|
42
|
+
raise NotImplementedError, "#{self.class} must implement #retrieve_notifications"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def format_notification(notification)
|
|
46
|
+
raise NotImplementedError, "#{self.class} must implement #format_notification"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def label
|
|
50
|
+
raise NotImplementedError, "#{self.class} must implement #label"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def updated_at
|
|
54
|
+
raise NotImplementedError, "#{self.class} must implement #updated_at"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def as_json(options = {})
|
|
58
|
+
{
|
|
59
|
+
label: label,
|
|
60
|
+
notifications: Array(fetch_notifications).map { |notification| format_notification(notification) }.compact
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def log_error(error)
|
|
67
|
+
Compass.logger&.error(
|
|
68
|
+
"[ Compass::Notification::Provider ] #{self.class}: #{error.class}: #{error.message}"
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
module Compass
|
|
2
|
+
# Compass::Menu test helper for RSpec
|
|
3
|
+
module Rspec::Menu
|
|
4
|
+
# Creates a matcher for menu items, allowing you to assert that a menu item
|
|
5
|
+
# with a given label exists.
|
|
6
|
+
#
|
|
7
|
+
# I.e.:
|
|
8
|
+
# menu = ExampleMenu.new
|
|
9
|
+
#
|
|
10
|
+
# expect(menu).to have_menu_item("Dashboard")
|
|
11
|
+
# expect(menu).to have_menu_item("Dashboard").with_url("/dashboard")
|
|
12
|
+
# expect(menu).to have_menu_item("Dashboard").with_icon("fa fa-dashboard")
|
|
13
|
+
# expect(menu).to have_menu_item("Dashboard").with_tags("tag1", "tag2")
|
|
14
|
+
# expect(menu).to have_menu_item("Dashboard").with_badge(1)
|
|
15
|
+
# expect(menu).to(
|
|
16
|
+
# have_menu_item("Dashboard")
|
|
17
|
+
# .with_badge(1)
|
|
18
|
+
# .with_icon("dashboard")
|
|
19
|
+
# )
|
|
20
|
+
#
|
|
21
|
+
#
|
|
22
|
+
# The matcher will test whether the given menu/item has a
|
|
23
|
+
# nested authorized item with the given attributes.
|
|
24
|
+
# To match unauthorized items, you can also pass `authorized: false`
|
|
25
|
+
# to `have_menu_item`.
|
|
26
|
+
#
|
|
27
|
+
# I.e.:
|
|
28
|
+
#
|
|
29
|
+
# menu = ExampleMenu.new
|
|
30
|
+
# expect(menu).to have_menu_item("Unauthorized Item", authorized: false)
|
|
31
|
+
#
|
|
32
|
+
# @param label [String] the label of the menu item
|
|
33
|
+
# @param url [String] the URL of the menu item
|
|
34
|
+
# @param badge [String] the badge of the menu item
|
|
35
|
+
# @param icon [String] the icon of the menu item
|
|
36
|
+
# @param tags [Array<String>] the tags of the menu item
|
|
37
|
+
# @param authorized [Boolean] whether the menu item is authorized
|
|
38
|
+
# @return [HaveMenuItem] the matcher
|
|
39
|
+
def have_menu_item(...) # rubocop:disable Naming/PredicateName
|
|
40
|
+
HaveMenuItem.new(...)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class HaveMenuItem
|
|
44
|
+
def initialize(label, url: nil, badge: nil, icon: nil, tags: nil, authorized: true)
|
|
45
|
+
@label = label
|
|
46
|
+
@attributes = {
|
|
47
|
+
url: url,
|
|
48
|
+
authorized: authorized,
|
|
49
|
+
badge: badge,
|
|
50
|
+
icon: icon,
|
|
51
|
+
tags: tags
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def matches?(menu_item)
|
|
56
|
+
@menu_item = menu_item
|
|
57
|
+
matching_label = menu_item.items.find do |item|
|
|
58
|
+
item.label == @label
|
|
59
|
+
end
|
|
60
|
+
if matching_label
|
|
61
|
+
@matcher = RSpec::Matchers::BuiltIn::HaveAttributes.new(@attributes.compact)
|
|
62
|
+
def @matcher.actual_formatted = @actual.label.inspect
|
|
63
|
+
@matcher.matches?(matching_label)
|
|
64
|
+
else
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Matches the item URL
|
|
70
|
+
def with_url(url)
|
|
71
|
+
@attributes[:url] = url
|
|
72
|
+
self
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Matches the item icon
|
|
76
|
+
def with_icon(icon)
|
|
77
|
+
@attributes[:icon] = icon
|
|
78
|
+
self
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Matches the item tags
|
|
82
|
+
def with_tags(*tags)
|
|
83
|
+
@attributes[:tags] = tags.flatten
|
|
84
|
+
self
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Matches the item badge
|
|
88
|
+
def with_badge(badge)
|
|
89
|
+
@attributes[:badge] = badge
|
|
90
|
+
self
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @private
|
|
94
|
+
def failure_message
|
|
95
|
+
return @matcher.failure_message if @matcher
|
|
96
|
+
|
|
97
|
+
"expected #{@menu_item.label} to have nested item #{@label}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @private
|
|
101
|
+
def failure_message_when_negated
|
|
102
|
+
return @matcher.failure_message_when_negated if @matcher
|
|
103
|
+
|
|
104
|
+
"expected #{@menu_item.label} to not have nested item #{@label}"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
module Compass
|
|
2
|
+
module Search
|
|
3
|
+
class UnknownProvider < StandardError
|
|
4
|
+
attr_reader :provider_name, :available_providers
|
|
5
|
+
|
|
6
|
+
def initialize(provider_name, available_providers)
|
|
7
|
+
@provider_name = provider_name
|
|
8
|
+
@available_providers = available_providers
|
|
9
|
+
super("Provider '#{provider_name}' not found")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class Provider
|
|
14
|
+
include Compass::Search::Rendering
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
attr_reader :providers
|
|
18
|
+
|
|
19
|
+
# Handles both class objects and string class names
|
|
20
|
+
def constantize_provider(provider)
|
|
21
|
+
provider.is_a?(String) ? provider.constantize : provider
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Aggregates search results from all registered providers
|
|
25
|
+
def global_search(query, context = {})
|
|
26
|
+
Compass.config.search.providers.flat_map do |provider|
|
|
27
|
+
begin
|
|
28
|
+
provider_class = constantize_provider(provider)
|
|
29
|
+
provider_class.new(**context).search(query)
|
|
30
|
+
rescue => e
|
|
31
|
+
Compass.logger&.error("[ Compass::Search::Provider ] #{provider_class}: #{e.class}: #{e.message}")
|
|
32
|
+
[]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Add this new class method for individual provider search
|
|
38
|
+
def search(query, provider_name, context = {})
|
|
39
|
+
provider_class = find_by_name(provider_name)
|
|
40
|
+
if provider_class
|
|
41
|
+
provider_class.new(**context).search(query)
|
|
42
|
+
else
|
|
43
|
+
raise UnknownProvider.new(provider_name, available_provider_names)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def normalize_name(provider_class)
|
|
48
|
+
provider_class.name.demodulize.underscore.sub(/_?provider$/, "")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def find_by_name(name)
|
|
52
|
+
Compass.config.search.providers.find do |provider|
|
|
53
|
+
provider_class = constantize_provider(provider)
|
|
54
|
+
normalize_name(provider_class) == name.to_s
|
|
55
|
+
end&.then { |p| constantize_provider(p) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def available_provider_names
|
|
59
|
+
Compass.config.search.providers.map do |provider|
|
|
60
|
+
provider_class = constantize_provider(provider)
|
|
61
|
+
normalize_name(provider_class)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def initialize(**context)
|
|
67
|
+
@context = context
|
|
68
|
+
context.each { |key, value| define_singleton_method(key) { value } }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def search(query)
|
|
72
|
+
records = fetch_records(query)
|
|
73
|
+
{
|
|
74
|
+
label: label,
|
|
75
|
+
search_options: search_options,
|
|
76
|
+
results: Array(records).map { |record| format_result(record) }.compact
|
|
77
|
+
}
|
|
78
|
+
rescue => e
|
|
79
|
+
log_error(e, query)
|
|
80
|
+
{
|
|
81
|
+
label: label,
|
|
82
|
+
search_options: search_options,
|
|
83
|
+
results: []
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def label
|
|
88
|
+
self.class.name.demodulize.sub(/Provider$/, "").titleize
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def search_options
|
|
92
|
+
{
|
|
93
|
+
keys: [ { name: "label", weight: 1 } ],
|
|
94
|
+
threshold: 0.2
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def fetch_records(query)
|
|
99
|
+
raise NotImplementedError, "#{self.class} must implement #fetch_records"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def format_result(record)
|
|
103
|
+
{
|
|
104
|
+
id: record.try(:id),
|
|
105
|
+
label: record.try(:label) || record.try(:name) || record.try(:title) || record.try(:to_s),
|
|
106
|
+
category: record.try(:category) || record.class.name.demodulize,
|
|
107
|
+
tag: record.try(:tag) || record.try(:role)
|
|
108
|
+
}.compact
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def log_error(error, query)
|
|
112
|
+
Compass.logger&.error(
|
|
113
|
+
"[ Compass::Search::Provider ] #{self.class}: #{error.class}: #{error.message} | Query: #{query.inspect}"
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Compass
|
|
2
|
+
module Search
|
|
3
|
+
module Rendering
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
class_attribute :helpers, default: []
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class_methods do
|
|
11
|
+
def helper(*mods)
|
|
12
|
+
self.helpers += mods
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def view_context
|
|
17
|
+
@view_context ||= ViewContext.new.tap do |ctx|
|
|
18
|
+
self.class.helpers.each { |mod| ctx.extend(mod) }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def render(partial, locals = {})
|
|
23
|
+
view_context.render(partial: partial, locals: { **@context, **locals })
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Compass
|
|
2
|
+
module Search
|
|
3
|
+
class ViewContext < ActionView::Base
|
|
4
|
+
def initialize(view_paths = [])
|
|
5
|
+
all_view_paths = Compass.config.search.view_paths + Array(view_paths)
|
|
6
|
+
super ActionView::LookupContext.new(all_view_paths), {}, nil
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def view_paths
|
|
10
|
+
Compass.config.search.view_paths
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def compiled_method_container
|
|
14
|
+
self.class.compiled_method_container
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.compiled_method_container
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/compass.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require "compass/configuration"
|
|
2
|
+
require "compass/version"
|
|
3
|
+
|
|
4
|
+
# Compass implements the Compass communication protocol, allowing apps
|
|
5
|
+
# to integrate to a Compass ecossystem, exposing Menu, Search, and other
|
|
6
|
+
# features.
|
|
7
|
+
module Compass
|
|
8
|
+
class << self
|
|
9
|
+
attr_accessor :logger
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
autoload :Menu, "compass/menu"
|
|
13
|
+
autoload :Search, "compass/search"
|
|
14
|
+
autoload :Notification, "compass/notification"
|
|
15
|
+
autoload :Breadcrumb, "compass/breadcrumb"
|
|
16
|
+
autoload :LazyAttribute, "compass/lazy_attribute"
|
|
17
|
+
|
|
18
|
+
include Compass::Configuration
|
|
19
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: power-compass
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Carlos Palhares
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-10-24 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rails
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.0'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '8.0'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '7.0'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '8.0'
|
|
33
|
+
description: Compass provides backend services for Compass:UI
|
|
34
|
+
email:
|
|
35
|
+
- chjunior@gmail.com
|
|
36
|
+
executables: []
|
|
37
|
+
extensions: []
|
|
38
|
+
extra_rdoc_files: []
|
|
39
|
+
files:
|
|
40
|
+
- MIT-LICENSE
|
|
41
|
+
- README.md
|
|
42
|
+
- Rakefile
|
|
43
|
+
- app/controllers/compass/application_controller.rb
|
|
44
|
+
- app/controllers/compass/menu_controller.rb
|
|
45
|
+
- app/controllers/compass/notification_controller.rb
|
|
46
|
+
- app/controllers/compass/search_controller.rb
|
|
47
|
+
- config/routes.rb
|
|
48
|
+
- lib/compass.rb
|
|
49
|
+
- lib/compass/breadcrumb.rb
|
|
50
|
+
- lib/compass/breadcrumb/middleware.rb
|
|
51
|
+
- lib/compass/configuration.rb
|
|
52
|
+
- lib/compass/configuration/breadcrumb.rb
|
|
53
|
+
- lib/compass/configuration/menu.rb
|
|
54
|
+
- lib/compass/configuration/notification.rb
|
|
55
|
+
- lib/compass/configuration/search.rb
|
|
56
|
+
- lib/compass/engine.rb
|
|
57
|
+
- lib/compass/lazy_attribute.rb
|
|
58
|
+
- lib/compass/menu.rb
|
|
59
|
+
- lib/compass/menu/item.rb
|
|
60
|
+
- lib/compass/menu/item_list.rb
|
|
61
|
+
- lib/compass/notification.rb
|
|
62
|
+
- lib/compass/notification/provider.rb
|
|
63
|
+
- lib/compass/rspec.rb
|
|
64
|
+
- lib/compass/rspec/menu.rb
|
|
65
|
+
- lib/compass/search.rb
|
|
66
|
+
- lib/compass/search/provider.rb
|
|
67
|
+
- lib/compass/search/rendering.rb
|
|
68
|
+
- lib/compass/search/view_context.rb
|
|
69
|
+
- lib/compass/version.rb
|
|
70
|
+
- lib/tasks/compass_tasks.rake
|
|
71
|
+
homepage: https://powerhrg.com
|
|
72
|
+
licenses:
|
|
73
|
+
- MIT
|
|
74
|
+
metadata:
|
|
75
|
+
homepage_uri: https://powerhrg.com
|
|
76
|
+
source_code_uri: https://powerhrg.com
|
|
77
|
+
changelog_uri: https://powerhrg.com
|
|
78
|
+
post_install_message:
|
|
79
|
+
rdoc_options: []
|
|
80
|
+
require_paths:
|
|
81
|
+
- lib
|
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
83
|
+
requirements:
|
|
84
|
+
- - ">="
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: '0'
|
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
88
|
+
requirements:
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '0'
|
|
92
|
+
requirements: []
|
|
93
|
+
rubygems_version: 3.5.22
|
|
94
|
+
signing_key:
|
|
95
|
+
specification_version: 4
|
|
96
|
+
summary: Compass provides backend services for Compass:UI
|
|
97
|
+
test_files: []
|