avo 1.4.4 → 1.4.5.pre.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of avo might be problematic. Click here for more details.

Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -0
  3. data/Gemfile.lock +7 -1
  4. data/README.md +4 -4
  5. data/app/components/avo/views/resource_index_component.html.erb +3 -3
  6. data/app/controllers/avo/application_controller.rb +14 -7
  7. data/app/controllers/avo/relations_controller.rb +1 -1
  8. data/app/controllers/avo/search_controller.rb +50 -35
  9. data/app/packs/entrypoints/application.css +2 -1
  10. data/app/packs/entrypoints/application.js +3 -0
  11. data/app/packs/js/controllers/search_controller.js +120 -0
  12. data/app/packs/js/helpers/debounce_promise.js +13 -0
  13. data/app/packs/stylesheets/search.css +79 -0
  14. data/app/views/avo/partials/_global_search.html.erb +17 -0
  15. data/app/views/avo/partials/_profile_dropdown.html.erb +8 -12
  16. data/app/views/avo/partials/_resource_search.html.erb +10 -0
  17. data/app/views/layouts/avo/application.html.erb +2 -4
  18. data/avo.gemspec +1 -0
  19. data/config/routes.rb +2 -6
  20. data/lib/avo/app.rb +3 -1
  21. data/lib/avo/base_resource.rb +83 -7
  22. data/lib/avo/configuration.rb +2 -0
  23. data/lib/avo/fields/base_field.rb +6 -0
  24. data/lib/avo/fields/file_field.rb +4 -0
  25. data/lib/avo/licensing/license.rb +10 -0
  26. data/lib/avo/licensing/pro_license.rb +3 -1
  27. data/lib/avo/services/authorization_service.rb +1 -1
  28. data/lib/avo/version.rb +1 -1
  29. data/lib/generators/avo/templates/initializer/avo.tt +1 -0
  30. data/lib/generators/avo/templates/resource/resource.tt +3 -0
  31. data/lib/generators/avo/templates/tool/sidebar_item.tt +1 -1
  32. data/public/avo-packs/css/{application-68691c73.css → application-d69fe930.css} +183 -6
  33. data/public/avo-packs/css/application-d69fe930.css.br +0 -0
  34. data/public/avo-packs/css/application-d69fe930.css.gz +0 -0
  35. data/public/avo-packs/css/application-d69fe930.css.map +1 -0
  36. data/public/avo-packs/css/application-d69fe930.css.map.br +0 -0
  37. data/public/avo-packs/css/application-d69fe930.css.map.gz +0 -0
  38. data/public/avo-packs/js/application-7b3d507875f4bc1f6677.js +26 -0
  39. data/public/avo-packs/js/{application-6a0b9e58526ae6bef242.js.LICENSE.txt → application-7b3d507875f4bc1f6677.js.LICENSE.txt} +0 -0
  40. data/public/avo-packs/js/application-7b3d507875f4bc1f6677.js.br +0 -0
  41. data/public/avo-packs/js/application-7b3d507875f4bc1f6677.js.gz +0 -0
  42. data/public/avo-packs/js/application-7b3d507875f4bc1f6677.js.map +1 -0
  43. data/public/avo-packs/js/application-7b3d507875f4bc1f6677.js.map.br +0 -0
  44. data/public/avo-packs/js/application-7b3d507875f4bc1f6677.js.map.gz +0 -0
  45. data/public/avo-packs/manifest.json +15 -15
  46. metadata +36 -17
  47. data/public/avo-packs/css/application-68691c73.css.br +0 -0
  48. data/public/avo-packs/css/application-68691c73.css.gz +0 -0
  49. data/public/avo-packs/css/application-68691c73.css.map +0 -1
  50. data/public/avo-packs/css/application-68691c73.css.map.br +0 -0
  51. data/public/avo-packs/css/application-68691c73.css.map.gz +0 -0
  52. data/public/avo-packs/js/application-6a0b9e58526ae6bef242.js +0 -26
  53. data/public/avo-packs/js/application-6a0b9e58526ae6bef242.js.br +0 -0
  54. data/public/avo-packs/js/application-6a0b9e58526ae6bef242.js.gz +0 -0
  55. data/public/avo-packs/js/application-6a0b9e58526ae6bef242.js.map +0 -1
  56. data/public/avo-packs/js/application-6a0b9e58526ae6bef242.js.map.br +0 -0
  57. data/public/avo-packs/js/application-6a0b9e58526ae6bef242.js.map.gz +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ab528ed4c8361ffed46892c8e838897c9f834a6bba2ad4d5c2134a33381bcb0
4
- data.tar.gz: 6a01552f743f231a4e1de2a81a85814b9a5e4708a22c514f54e1f48df37b9319
3
+ metadata.gz: 46a795d39c0e6c43399df4c10b3a75891557f6e1d525ec737420573e3042521a
4
+ data.tar.gz: 970f8657291f6b9a0d1338265e9253120135128eb3a248c88ca4a457f7273d64
5
5
  SHA512:
6
- metadata.gz: 2d46d15e71d47b3eaf1f698572aa9f5edb2de0a5699926ef57373bce6f45b00afded60f8cf74016a1d393cadddf3aa807ea16fe7a6e4f3812f44b04ac9ce2bb7
7
- data.tar.gz: 8d62dfe3b1e41ee0a91281b848eb5a5af48738d5c85bd25c94c0167813df1a5378b3b9393d7d0778630757ccb336801382e8b59d8769b790825138699ddbff32
6
+ metadata.gz: c0886bf3479f6eaf19d674b60e81906714666433d3b90d5b3685fac16ed558a18aeb099826ffc8e8ba798fa4149586ee36b4c447c6c05ce1141effd0c144a5a8
7
+ data.tar.gz: d8afee4446cef7180453beb74ef91cdf68f860e59ae8baa62c660075c4de8656ecabf2c6668318a21a85ca410112caa3b39b43b5f88cbaea5fbe3d1bd6b8ea21
data/Gemfile CHANGED
@@ -125,3 +125,5 @@ gem 'meta-tags'
125
125
  gem 'breadcrumbs_on_rails'
126
126
 
127
127
  gem 'manifester'
128
+
129
+ gem 'ransack'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- avo (1.4.4)
4
+ avo (1.4.5.pre.1)
5
5
  active_link_to
6
6
  addressable
7
7
  breadcrumbs_on_rails
@@ -14,6 +14,7 @@ PATH
14
14
  pagy
15
15
  pundit
16
16
  rails (>= 6.0)
17
+ ransack
17
18
  view_component
18
19
  zeitwerk
19
20
 
@@ -235,6 +236,10 @@ GEM
235
236
  thor (>= 0.20.3, < 2.0)
236
237
  rainbow (3.0.0)
237
238
  rake (13.0.3)
239
+ ransack (2.4.2)
240
+ activerecord (>= 5.2.4)
241
+ activesupport (>= 5.2.4)
242
+ i18n
238
243
  rb-fsevent (0.10.4)
239
244
  rb-inotify (0.10.1)
240
245
  ffi (~> 1.0)
@@ -390,6 +395,7 @@ DEPENDENCIES
390
395
  pundit
391
396
  rails (~> 6.0.2, >= 6.0.2.2)
392
397
  rails-controller-testing
398
+ ransack
393
399
  rspec-rails (~> 4.0.0)
394
400
  ruby-debug-ide
395
401
  sass-rails (>= 6)
data/README.md CHANGED
@@ -29,13 +29,13 @@ Avo is a beautiful next-generation framework that empowers you, the developer, t
29
29
  - **Actions** - Run custom actions to one or more of your resources with as little as pressing a button 💪
30
30
  - **Filters** - Write your own custom filters to quickly segment your data.
31
31
  - **Keeps your app clean** - You don't need to change your app to use Avo. Drop it in your existing app or add it to a new one and you're done 🙌
32
- - **Custom fields***- No worries if we missed a field you need. Generate a custom field in a jiffy.
33
- - **Dashboard widgets and metrics*** - Customize your dashboard with the tools and analytics you need.
32
+ - **Custom fields**- No worries if we missed a field you need. Generate a custom field in a jiffy.
34
33
  - **Custom tools** - You need to add a page with something completely new, you've got it!
35
34
  - **Authorization** - Leverage Pundit policies to build a robust and scalable authorization system.
36
- - **Themable*** - Dress it up into your own colors.
37
- - **Localization*** - Have it available in any language you need.
35
+ - **Localization** - Have it available in any language you need.
38
36
  - **No asset pipeline pollution** - Bring your own asset pipeline.
37
+ - **Dashboard widgets and metrics*** - Customize your dashboard with the tools and analytics you need.
38
+ - **Themable*** - Dress it up into your own colors.
39
39
 
40
40
  *Features still under development
41
41
 
@@ -22,12 +22,12 @@
22
22
  <% end %>
23
23
 
24
24
  <% c.body do %>
25
- <div class="flex justify-between py-6 min-h-24"
25
+ <div class="flex justify-between pt-2 pb-2 min-h-16"
26
26
  data-selected-resources-name="<%= @resource.plural_name.downcase %>"
27
27
  data-selected-resources="[]"
28
28
  >
29
29
  <div class="flex items-center px-6 w-64">
30
- <%# search %>
30
+ <%= render partial: 'avo/partials/resource_search', locals: {resource: @resource.route_key} if @resource.search_query.present? %>
31
31
  </div>
32
32
  <div class="flex justify-end items-center px-6 space-x-3">
33
33
  <%= render partial: 'avo/partials/view_toggle_button', locals: { available_view_types: available_view_types, view_type: view_type, turbo_frame: @turbo_frame } if @models.present? %>
@@ -36,7 +36,7 @@
36
36
  </div>
37
37
 
38
38
  <% if view_type.to_sym == :table %>
39
- <div class="w-full overflow-auto flex flex-col">
39
+ <div class="w-full overflow-auto flex flex-col mt-4">
40
40
  <div class="relative flex-1 flex">
41
41
  <%= render(Avo::Index::ResourceTableComponent.new(resources: @resources, resource: @resource, reflection: @reflection, parent_model: @parent_model)) %>
42
42
  </div>
@@ -17,7 +17,7 @@ module Avo
17
17
  add_flash_types :info, :warning, :success, :error
18
18
 
19
19
  def init_app
20
- Avo::App.init request: request, context: context
20
+ Avo::App.init request: request, context: context, current_user: _current_user
21
21
 
22
22
  @license = Avo::App.license
23
23
  end
@@ -38,18 +38,25 @@ module Avo
38
38
  def render(*args)
39
39
  raise Avo::LicenseVerificationTemperedError, "License verification mechanism tempered with." unless method(:check_avo_license).source_location.first.match?(/.*\/app\/controllers\/avo\/application_controller\.rb/)
40
40
 
41
+ if params[:controller] == "avo/search" && params[:action] == "index"
42
+ raise Avo::LicenseVerificationTemperedError, "License verification mechanism tempered with." unless method(:index).source_location.first.match?(/.*\/app\/controllers\/avo\/search_controller\.rb/)
43
+ end
44
+
41
45
  super(*args)
42
46
  end
43
47
 
44
48
  def check_avo_license
49
+ # Check to see if the path is on a custom tool
45
50
  unless on_root_path || on_resources_path || on_api_path
46
- if @license.invalid? || @license.lacks(:custom_tools)
47
- if Rails.env.development?
51
+ # Display alert on custom tool page if in development.
52
+ if @license.lacks(:custom_tools) || @license.invalid?
53
+ if Rails.env.development? || Rails.env.test?
48
54
  @custom_tools_alert_visible = true
49
- else
50
- raise Avo::LicenseInvalidError, "Your license is invalid or doesn't support custom tools."
51
55
  end
52
56
  end
57
+
58
+ # Raise error in non-development environments.
59
+ raise Avo::LicenseInvalidError, "Your license is invalid or doesn't support custom tools." if @license.lacks_with_trial(:custom_tools)
53
60
  end
54
61
  end
55
62
 
@@ -157,9 +164,9 @@ module Avo
157
164
 
158
165
  def authorize_action
159
166
  if @model.present?
160
- @authorization.set_record(@model).authorize_action :index
167
+ @authorization.set_record(@model).authorize_action action_name.to_sym
161
168
  else
162
- @authorization.set_record(@resource.model_class).authorize_action :index
169
+ @authorization.set_record(@resource.model_class).authorize_action action_name.to_sym
163
170
  end
164
171
  end
165
172
 
@@ -17,7 +17,7 @@ module Avo
17
17
  @resource = @related_resource
18
18
  @parent_model = @parent_resource.model_class.find(params[:id])
19
19
  @parent_resource.hydrate(model: @parent_model)
20
- @query = @parent_model.public_send(params[:related_name])
20
+ @query = @authorization.apply_policy @parent_model.public_send(params[:related_name])
21
21
 
22
22
  super
23
23
  end
@@ -2,56 +2,71 @@ require_dependency "avo/application_controller"
2
2
 
3
3
  module Avo
4
4
  class SearchController < ApplicationController
5
- # before_action :authorize_user
5
+ before_action :set_resource_name, only: [:show]
6
+ before_action :set_resource, only: [:show]
6
7
 
7
8
  def index
8
- @authorization.set_record(resource_model).authorize_action :index
9
- resources = []
10
-
11
- App.resources
12
- .select { |resource| resource.search.present? }
13
- .select { |resource| AuthorizationService.authorize_action _current_user, resource.model, "index" }
14
- .each do |resource_model|
15
- found_resources = add_link_to_search_results(search_resource(resource_model), resource_model)
16
- resources.push({
17
- label: resource_model.name,
18
- resources: found_resources
19
- })
20
- end
9
+ raise ActionController::BadRequest.new("This feature requires the pro license https://avohq.io/purchase/pro") if App.license.lacks_with_trial(:global_search)
21
10
 
22
- render json: {
23
- resources: resources
24
- }
11
+ render json: search_resources(Avo::App.resources)
25
12
  end
26
13
 
27
- def resource
28
- render json: {
29
- resources: add_link_to_search_results(search_resource(avo_resource), avo_resource)
30
- }
14
+ def show
15
+ render json: search_resources([resource])
31
16
  end
32
17
 
33
18
  private
34
19
 
35
- def add_link_to_search_results(resources, avo_resource)
36
- resources.map do |model|
37
- {
38
- id: model.id,
39
- search_label: model.send(avo_resource.class.title),
40
- link: "/resources/#{model.class.to_s.singularize.underscore}/#{model.id}"
41
- }
20
+ def search_resources(resources)
21
+ resources.map do |resource|
22
+ # Apply authorization
23
+ next unless @authorization.set_record(resource.model_class).authorize_action(:index, raise_exception: false)
24
+ # Filter out the models without a search_query
25
+ next if resource.search_query.nil?
26
+
27
+ search_resource resource
42
28
  end
29
+ .select do |payload|
30
+ payload.present?
31
+ end
32
+ .sort do |payload|
33
+ payload.last[:count]
34
+ end
35
+ .reverse
36
+ .to_h
43
37
  end
44
38
 
45
- def search_resource(avo_resource)
46
- avo_resource.query_search(query: params[:q], via_resource_name: params[:via_resource_name], via_resource_id: params[:via_resource_id], user: _current_user)
39
+ def search_resource(resource)
40
+ results = apply_search_metadata(resource.search_query.call(params: params).limit(8), resource)
41
+
42
+ result_object = {
43
+ header: resource.name.pluralize,
44
+ results: results,
45
+ count: results.length
46
+ }
47
+
48
+ [resource.name.pluralize.downcase, result_object]
47
49
  end
48
50
 
49
- # def authorize_user
50
- # return if params[:action] == 'index'
51
+ def apply_search_metadata(models, avo_resource)
52
+ models.map do |model|
53
+ resource = avo_resource.dup.hydrate(model: model).hydrate_fields(model: model)
51
54
 
52
- # action = params[:action] == 'resource' ? :index : params[:action]
55
+ result = {
56
+ _id: model.id,
57
+ _label: resource.label,
58
+ _url: resource.avo_path,
59
+ model: model
60
+ }
53
61
 
54
- # return render_unauthorized unless AuthorizationService::authorize_action _current_user, avo_resource.model, action
55
- # end
62
+ if App.license.has_with_trial(:enhanced_search_results)
63
+ result[:_description] = resource.description
64
+ result[:_avatar] = resource.avatar
65
+ result[:_avatar_type] = resource.avatar_type
66
+ end
67
+
68
+ result
69
+ end
70
+ end
56
71
  end
57
72
  end
@@ -4,7 +4,7 @@
4
4
  @import './../../../node_modules/tippy.js/themes/light.css';
5
5
  @import './../../../node_modules/trix/dist/trix.css';
6
6
  @import './../../../node_modules/flatpickr/dist/flatpickr.css';
7
-
7
+ @import './../../../node_modules/@algolia/autocomplete-theme-classic/dist/theme.css';
8
8
 
9
9
  @import './../stylesheets/tailwindcss/base.css';
10
10
 
@@ -15,6 +15,7 @@
15
15
  @import './../stylesheets/loader.css';
16
16
  @import './../stylesheets/pagination.css';
17
17
  @import './../stylesheets/breadcrumbs.css';
18
+ @import './../stylesheets/search.css';
18
19
 
19
20
  @import './../stylesheets/components/status.css';
20
21
  @import './../stylesheets/components/code.css';
@@ -45,6 +45,9 @@ document.addEventListener('turbo:load', () => {
45
45
  })
46
46
  document.addEventListener('turbo:visit', () => document.body.classList.add('turbo-loading'))
47
47
  document.addEventListener('turbo:submit-start', () => document.body.classList.add('turbo-loading'))
48
+ document.addEventListener('turbo:before-cache', () => {
49
+ document.querySelectorAll('[data-turbo-remove-before-cache]').forEach((element) => element.remove())
50
+ })
48
51
 
49
52
  // Uncomment to copy all static images under ../images to the output folder and reference
50
53
  // them with the image_pack_tag helper in views (e.g <%= image_pack_tag 'rails.png' %>)
@@ -0,0 +1,120 @@
1
+ /* eslint-disable no-underscore-dangle */
2
+ import * as Mousetrap from 'mousetrap'
3
+ import { Controller } from 'stimulus'
4
+ import { Turbo } from '@hotwired/turbo-rails'
5
+ import { autocomplete } from '@algolia/autocomplete-js'
6
+ import debouncePromise from '@/js/helpers/debounce_promise'
7
+
8
+ export default class extends Controller {
9
+ static targets = ['autocomplete', 'button']
10
+
11
+ debouncedFetch = debouncePromise(fetch, this.debounceTimeout)
12
+
13
+ get translationKeys() {
14
+ let keys
15
+ try {
16
+ keys = JSON.parse(this.autocompleteTarget.dataset.translationKeys)
17
+ } catch (error) {
18
+ keys = {}
19
+ }
20
+
21
+ return keys
22
+ }
23
+
24
+ get debounceTimeout() {
25
+ return this.autocompleteTarget.dataset.debounceTimeout
26
+ }
27
+
28
+ get searchResource() {
29
+ return this.autocompleteTarget.dataset.searchResource
30
+ }
31
+
32
+ get isGlobalSearch() {
33
+ return this.searchResource === 'global'
34
+ }
35
+
36
+ get searchUrl() {
37
+ return this.isGlobalSearch ? '/avo/avo_api/search' : `/avo/avo_api/${this.searchResource}/search`
38
+ }
39
+
40
+ addSource(resourceName, data) {
41
+ const that = this
42
+
43
+ return {
44
+ sourceId: resourceName,
45
+ getItems: () => data.results,
46
+ onSelect({ item }) {
47
+ Turbo.visit(item._url, { action: 'replace' })
48
+ },
49
+ templates: {
50
+ header() {
51
+ return data.header
52
+ },
53
+ item({ item, createElement }) {
54
+ let element = ''
55
+
56
+ if (item._avatar) {
57
+ let classes
58
+
59
+ switch (item._avatar_type) {
60
+ default:
61
+ case 'circle':
62
+ classes = 'rounded-full'
63
+ break
64
+ case 'rounded':
65
+ classes = 'rounded'
66
+ break
67
+ }
68
+
69
+ element += `<img src="${item._avatar}" alt="${item._label}" class="flex-shrink-0 w-8 h-8 my-[2px] inline mr-2 ${classes}" />`
70
+ }
71
+ element += `<div>${item._label}`
72
+
73
+ if (item._description) {
74
+ element += `<div class="aa-ItemDescription">${item._description}</div>`
75
+ }
76
+
77
+ element += '</div>'
78
+
79
+ return createElement('div', {
80
+ class: 'flex',
81
+ dangerouslySetInnerHTML: {
82
+ __html: element,
83
+ },
84
+ })
85
+ },
86
+ noResults() {
87
+ return that.translationKeys.no_item_found.replace('%{item}', resourceName)
88
+ },
89
+ },
90
+ }
91
+ }
92
+
93
+ showSearchPanel() {
94
+ this.autocompleteTarget.querySelector('button').click()
95
+ }
96
+
97
+ connect() {
98
+ const that = this
99
+
100
+ this.buttonTarget.onclick = () => this.showSearchPanel()
101
+
102
+ if (this.isGlobalSearch) {
103
+ Mousetrap.bind(['command+k', 'ctrl+k'], () => this.showSearchPanel())
104
+ }
105
+
106
+ autocomplete({
107
+ container: this.autocompleteTarget,
108
+ placeholder: 'Search',
109
+ openOnFocus: true,
110
+ detachedMediaQuery: '',
111
+ getSources: ({ query }) => {
112
+ const endpoint = `${that.searchUrl}?q=${query}`
113
+
114
+ return that.debouncedFetch(endpoint)
115
+ .then((response) => response.json())
116
+ .then((data) => Object.keys(data).map((resourceName) => that.addSource(resourceName, data[resourceName])))
117
+ },
118
+ })
119
+ }
120
+ }
@@ -0,0 +1,13 @@
1
+ export default (fn, time) => {
2
+ let timerId
3
+
4
+ return (...args) => {
5
+ if (timerId) {
6
+ clearTimeout(timerId)
7
+ }
8
+
9
+ return new Promise((resolve) => {
10
+ timerId = setTimeout(() => resolve(fn(...args)), time)
11
+ })
12
+ }
13
+ }
@@ -0,0 +1,79 @@
1
+ :root {
2
+ --aa-primary-color: --tw-ring-color;
3
+ --aa-selected-color: --tw-ring-color;
4
+ --aa-primary-color-rgb: 5, 150, 105;
5
+ }
6
+
7
+ .global-search {
8
+ .aa-DetachedSearchButton:focus,
9
+ .aa-DetachedSearchButton {
10
+ border: none !important;
11
+ }
12
+ }
13
+
14
+ .resource-search {
15
+ .aa-Autocomplete {
16
+ @apply w-full;
17
+ }
18
+
19
+ .aa-DetachedSearchButton {
20
+ @apply rounded-full border-gray-300;
21
+ }
22
+ }
23
+
24
+ .aa-SourceHeader {
25
+ @apply uppercase text-xs font-semibold;
26
+ }
27
+
28
+ .aa-DetachedFormContainer,
29
+ .aa-DetachedContainer .aa-Panel {
30
+ @apply bg-blue-gray-100 border-b-0;
31
+ }
32
+
33
+ .aa-Form {
34
+ &:focus-within {
35
+ @apply ring-0 !important;
36
+ }
37
+
38
+ input {
39
+ @apply focus:ring-0 !important;
40
+ }
41
+ &:focus-within {
42
+ .aa-InputWrapperPrefix{
43
+ .aa-SubmitButton {
44
+ @apply ring-0;
45
+ }
46
+ }
47
+ }
48
+ }
49
+ .aa-Input {
50
+ @apply focus:ring-green-600;
51
+ }
52
+
53
+ .aa-DetachedContainer {
54
+ .aa-PanelLayout {
55
+ @apply pt-0 space-y-3;
56
+ }
57
+
58
+ .aa-Source {
59
+ .aa-List {
60
+ @apply space-y-1;
61
+
62
+ .aa-Item {
63
+ @apply bg-white rounded-md px-3 py-4 shadow;
64
+
65
+ .aa-ItemDescription {
66
+ @apply text-gray-500 text-sm;
67
+ }
68
+
69
+ &[aria-selected=true]{
70
+ @apply bg-blue-700 text-white;
71
+
72
+ .aa-ItemDescription {
73
+ @apply text-white;
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }