avo 2.5.2.pre.1 → 2.5.2.pre.2
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.
- checksums.yaml +4 -4
- data/Gemfile +2 -0
- data/Gemfile.lock +4 -1
- data/app/assets/builds/avo.css +727 -21
- data/app/assets/builds/avo.js +211 -122
- data/app/assets/builds/avo.js.map +3 -3
- data/app/assets/stylesheets/avo.css +2 -0
- data/app/assets/stylesheets/css/tags.css +16 -0
- data/app/components/avo/card_component.rb +0 -12
- data/app/components/avo/fields/tags_field/edit_component.html.erb +27 -0
- data/app/components/avo/fields/tags_field/edit_component.rb +4 -0
- data/app/components/avo/fields/tags_field/index_component.html.erb +14 -0
- data/app/components/avo/fields/tags_field/index_component.rb +7 -0
- data/app/components/avo/fields/tags_field/show_component.html.erb +7 -0
- data/app/components/avo/fields/tags_field/show_component.rb +11 -0
- data/app/components/avo/fields/tags_field/tag_component.html.erb +9 -0
- data/app/components/avo/fields/tags_field/tag_component.rb +11 -0
- data/app/components/avo/index/resource_table_component.html.erb +1 -1
- data/app/javascript/js/application.js +1 -1
- data/app/javascript/js/controllers/base_controller.js +22 -0
- data/app/javascript/js/controllers/fields/tags_field_controller.js +86 -0
- data/app/javascript/js/controllers/fields/tags_field_helpers.js +47 -0
- data/app/javascript/js/controllers.js +2 -0
- data/app/views/avo/dashboards/_chartkick_card.html.erb +1 -1
- data/app/views/avo/dashboards/_metric_card.html.erb +1 -1
- data/db/factories.rb +2 -0
- data/lib/avo/base_resource.rb +6 -0
- data/lib/avo/concerns/handles_field_args.rb +36 -0
- data/lib/avo/fields/base_field.rb +2 -1
- data/lib/avo/fields/tags_field.rb +82 -0
- data/lib/avo/hosts/record_host.rb +7 -0
- data/lib/avo/licensing/pro_license.rb +2 -1
- data/lib/avo/version.rb +1 -1
- data/lib/generators/avo/templates/cards/chartkick_card.tt +1 -1
- data/lib/generators/avo/templates/cards/chartkick_card_sample.tt +1 -1
- data/lib/generators/avo/templates/cards/metric_card.tt +1 -1
- data/lib/generators/avo/templates/cards/metric_card_sample.tt +1 -1
- data/lib/generators/avo/templates/locales/avo.en.yml +4 -0
- data/public/avo-assets/avo.css +727 -21
- data/public/avo-assets/avo.js +211 -122
- data/public/avo-assets/avo.js.map +3 -3
- metadata +17 -2
@@ -4,6 +4,7 @@
|
|
4
4
|
@import './../../../node_modules/trix/dist/trix.css';
|
5
5
|
@import './../../../node_modules/flatpickr/dist/flatpickr.css';
|
6
6
|
@import './../../../node_modules/@algolia/autocomplete-theme-classic/dist/theme.css';
|
7
|
+
@import './../../../node_modules/@yaireo/tagify/dist/tagify.css';
|
7
8
|
|
8
9
|
@import 'tailwindcss/base';
|
9
10
|
|
@@ -18,6 +19,7 @@
|
|
18
19
|
@import './css/search.css';
|
19
20
|
@import './css/active-storage.css';
|
20
21
|
@import './css/spinner.css';
|
22
|
+
@import './css/tags.css';
|
21
23
|
|
22
24
|
@import './css/components/status.css';
|
23
25
|
@import './css/components/code.css';
|
@@ -3,21 +3,9 @@
|
|
3
3
|
class Avo::CardComponent < ViewComponent::Base
|
4
4
|
def initialize(card: nil)
|
5
5
|
@card = card
|
6
|
-
|
7
|
-
init_card
|
8
6
|
end
|
9
7
|
|
10
8
|
def render?
|
11
9
|
!@card.nil?
|
12
10
|
end
|
13
|
-
|
14
|
-
# Initializing the card byt running the query method.
|
15
|
-
# We'll still keep the query block around for compatibility reasons.
|
16
|
-
def init_card
|
17
|
-
if @card.respond_to? :query
|
18
|
-
@card.query
|
19
|
-
elsif @card.query_block.present?
|
20
|
-
@card.compute_result
|
21
|
-
end
|
22
|
-
end
|
23
11
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
<%= edit_field_wrapper field: @field, index: @index, form: @form, resource: @resource, displayed_in_modal: @displayed_in_modal do %>
|
2
|
+
<div data-controller="tags-field">
|
3
|
+
<%# dummy field %>
|
4
|
+
<%= text_field_tag "#{@field.id}-dummy", '',
|
5
|
+
class: helpers.input_classes('w-full', has_error: @field.model_errors.include?(@field.id)),
|
6
|
+
placeholder: @field.placeholder,
|
7
|
+
disabled: @field.readonly,
|
8
|
+
value: '',
|
9
|
+
data: {
|
10
|
+
'tags-field-target': 'fakeInput',
|
11
|
+
} %>
|
12
|
+
<%# real field %>
|
13
|
+
<%= @form.text_field @field.id,
|
14
|
+
class: helpers.input_classes('hidden w-full', has_error: @field.model_errors.include?(@field.id)),
|
15
|
+
placeholder: @field.placeholder,
|
16
|
+
disabled: @field.readonly,
|
17
|
+
value: @field.field_value.to_json,
|
18
|
+
data: {
|
19
|
+
'tags-field-target': 'input',
|
20
|
+
'whitelist-items': @field.suggestions.to_json,
|
21
|
+
'blacklist-items': @field.blacklist.to_json,
|
22
|
+
'enforce-suggestions': @field.enforce_suggestions ? 1 : 0,
|
23
|
+
'delimiters': @field.delimiters,
|
24
|
+
'close-on-select': @field.close_on_select ? 1 : 0,
|
25
|
+
} %>
|
26
|
+
</div>
|
27
|
+
<% end %>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<%= index_field_wrapper field: @field do %>
|
2
|
+
<div class="flex gap-1 items-center flex-nowrap">
|
3
|
+
<% value.take(3).each do |item| %>
|
4
|
+
<% if @field.acts_as_taggable_on.present? %>
|
5
|
+
<%= render Avo::Fields::TagsField::TagComponent.new(label: item['value']) %>
|
6
|
+
<% else %>
|
7
|
+
<%= render Avo::Fields::TagsField::TagComponent.new(label: item) %>
|
8
|
+
<% end %>
|
9
|
+
<% end %>
|
10
|
+
<% if value.count > 3 %>
|
11
|
+
<%= render Avo::Fields::TagsField::TagComponent.new(label: '...', title: I18n.t('avo.x_items_more', count: value.count - 3)) %>
|
12
|
+
<% end %>
|
13
|
+
</div>
|
14
|
+
<% end %>
|
@@ -0,0 +1,7 @@
|
|
1
|
+
<%= show_field_wrapper field: @field, index: @index do %>
|
2
|
+
<div class="flex gap-1 items-center flex-wrap">
|
3
|
+
<% @field.field_value.each do |item| %>
|
4
|
+
<%= render Avo::Fields::TagsField::TagComponent.new(label: label_from_item(item)) %>
|
5
|
+
<% end %>
|
6
|
+
</div>
|
7
|
+
<% end %>
|
@@ -1,5 +1,5 @@
|
|
1
1
|
<div class="w-full ">
|
2
|
-
<table class="w-full px-4 bg-white
|
2
|
+
<table class="w-full px-4 bg-white" data-resource-name='<%= @resource.model_key %>' data-controller='item-select-all'>
|
3
3
|
<%= render partial: 'avo/partials/table_header', locals: {fields: @resource.get_fields(reflection: @reflection)} %>
|
4
4
|
<tbody class="divide-y">
|
5
5
|
<% @resources.each_with_index do |resource, index| %>
|
@@ -4,7 +4,7 @@ import { Application } from '@hotwired/stimulus'
|
|
4
4
|
const application = Application.start()
|
5
5
|
|
6
6
|
// Configure Stimulus development experience
|
7
|
-
application.debug =
|
7
|
+
application.debug = window?.localStorage.getItem('avo.debug')
|
8
8
|
window.Stimulus = application
|
9
9
|
|
10
10
|
// Register stimulus-components controller
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import { Controller } from '@hotwired/stimulus'
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
/**
|
5
|
+
* Helper that parses the data attribute value to JSON
|
6
|
+
*/
|
7
|
+
getJsonAttribute(target, attribute, defaultValue = []) {
|
8
|
+
let result = defaultValue
|
9
|
+
try {
|
10
|
+
result = JSON.parse(target.getAttribute(attribute))
|
11
|
+
} catch (error) {}
|
12
|
+
|
13
|
+
return result
|
14
|
+
}
|
15
|
+
|
16
|
+
/**
|
17
|
+
* Parses the attribute to boolean
|
18
|
+
*/
|
19
|
+
getBooleanAttribute(target, attribute) {
|
20
|
+
return target.getAttribute(attribute) === '1'
|
21
|
+
}
|
22
|
+
}
|
@@ -0,0 +1,86 @@
|
|
1
|
+
import { first, isObject, merge } from 'lodash'
|
2
|
+
import Tagify from '@yaireo/tagify'
|
3
|
+
|
4
|
+
import BaseController from '../base_controller'
|
5
|
+
|
6
|
+
import { suggestionItemTemplate, tagTemplate } from './tags_field_helpers'
|
7
|
+
|
8
|
+
export default class extends BaseController {
|
9
|
+
static targets = ['input', 'fakeInput'];
|
10
|
+
|
11
|
+
tagify = null;
|
12
|
+
|
13
|
+
get whitelistItems() {
|
14
|
+
return this.getJsonAttribute(this.inputTarget, 'data-whitelist-items', [])
|
15
|
+
}
|
16
|
+
|
17
|
+
get blacklistItems() {
|
18
|
+
return this.getJsonAttribute(this.inputTarget, 'data-blacklist-items', [])
|
19
|
+
}
|
20
|
+
|
21
|
+
get enforceSuggestions() {
|
22
|
+
return this.getBooleanAttribute(this.inputTarget, 'data-enforce-suggestions')
|
23
|
+
}
|
24
|
+
|
25
|
+
get closeOnSelect() {
|
26
|
+
return this.getBooleanAttribute(this.inputTarget, 'data-close-on-select')
|
27
|
+
}
|
28
|
+
|
29
|
+
get delimiters() {
|
30
|
+
return this.getJsonAttribute(this.inputTarget, 'data-delimiters', [])
|
31
|
+
}
|
32
|
+
|
33
|
+
get suggestionsAreObjects() {
|
34
|
+
return isObject(first(this.whitelistItems))
|
35
|
+
}
|
36
|
+
|
37
|
+
get tagifyOptions() {
|
38
|
+
let options = {
|
39
|
+
whitelist: this.whitelistItems,
|
40
|
+
blacklist: this.blacklistItems,
|
41
|
+
enforceWhitelist: this.enforceSuggestions,
|
42
|
+
delimiters: this.delimiters.join('|'),
|
43
|
+
maxTags: 10,
|
44
|
+
dropdown: {
|
45
|
+
maxItems: 20,
|
46
|
+
enabled: 0,
|
47
|
+
closeOnSelect: this.closeOnSelect,
|
48
|
+
},
|
49
|
+
}
|
50
|
+
|
51
|
+
if (this.suggestionsAreObjects) {
|
52
|
+
options = merge(options, {
|
53
|
+
tagTextProp: 'label',
|
54
|
+
dropdown: {
|
55
|
+
searchKeys: ['label'],
|
56
|
+
},
|
57
|
+
templates: {
|
58
|
+
tag: tagTemplate,
|
59
|
+
dropdownItem: suggestionItemTemplate,
|
60
|
+
},
|
61
|
+
})
|
62
|
+
}
|
63
|
+
|
64
|
+
return options
|
65
|
+
}
|
66
|
+
|
67
|
+
connect() {
|
68
|
+
if (this.hasInputTarget) {
|
69
|
+
this.hideFakeInput()
|
70
|
+
this.showRealInput()
|
71
|
+
this.initTagify()
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
initTagify() {
|
76
|
+
this.tagify = new Tagify(this.inputTarget, this.tagifyOptions)
|
77
|
+
}
|
78
|
+
|
79
|
+
hideFakeInput() {
|
80
|
+
this.fakeInputTarget.classList.add('hidden')
|
81
|
+
}
|
82
|
+
|
83
|
+
showRealInput() {
|
84
|
+
this.inputTarget.classList.remove('hidden')
|
85
|
+
}
|
86
|
+
}
|
@@ -0,0 +1,47 @@
|
|
1
|
+
export function tagTemplate(tagData) {
|
2
|
+
const suggestions = this.settings.whitelist || []
|
3
|
+
|
4
|
+
const possibleSuggestion = suggestions.find(
|
5
|
+
// eslint-disable-next-line eqeqeq
|
6
|
+
(item) => item.value == tagData.value,
|
7
|
+
)
|
8
|
+
const possibleLabel = possibleSuggestion
|
9
|
+
? possibleSuggestion.label
|
10
|
+
: tagData.value
|
11
|
+
|
12
|
+
return `
|
13
|
+
<tag title="${tagData.value}"
|
14
|
+
contenteditable='false'
|
15
|
+
spellcheck='false'
|
16
|
+
tabIndex="-1"
|
17
|
+
class="tagify__tag ${tagData.class ? tagData.class : ''}"
|
18
|
+
${this.getAttributes(tagData)}
|
19
|
+
>
|
20
|
+
<x title='' class='tagify__tag__removeBtn' role='button' aria-label='remove tag'></x>
|
21
|
+
<div>
|
22
|
+
<span class='tagify__tag-text'>${possibleLabel}</span>
|
23
|
+
</div>
|
24
|
+
</tag>
|
25
|
+
`
|
26
|
+
}
|
27
|
+
|
28
|
+
export function suggestionItemTemplate(tagData) {
|
29
|
+
return `
|
30
|
+
<div ${this.getAttributes(tagData)}
|
31
|
+
class='tagify__dropdown__item flex items-center ${
|
32
|
+
tagData.class ? tagData.class : ''
|
33
|
+
}'
|
34
|
+
tabindex="0"
|
35
|
+
role="option">
|
36
|
+
${
|
37
|
+
tagData.avatar
|
38
|
+
? `
|
39
|
+
<div class='rounded w-8 h-8 block mr-2'>
|
40
|
+
<img onerror="this.style.visibility='hidden'" class="w-full" src="${tagData.avatar}">
|
41
|
+
</div>`
|
42
|
+
: ''
|
43
|
+
}
|
44
|
+
<span>${tagData.label}</span>
|
45
|
+
</div>
|
46
|
+
`
|
47
|
+
}
|
@@ -25,6 +25,7 @@ import SearchController from './controllers/search_controller'
|
|
25
25
|
import SelectController from './controllers/select_controller'
|
26
26
|
import SelectFilterController from './controllers/select_filter_controller'
|
27
27
|
import SimpleMdeController from './controllers/fields/simple_mde_controller'
|
28
|
+
import TagsFieldController from './controllers/fields/tags_field_controller'
|
28
29
|
import TextFilterController from './controllers/text_filter_controller'
|
29
30
|
import TippyController from './controllers/tippy_controller'
|
30
31
|
import TogglePanelController from './controllers/toggle_panel_controller'
|
@@ -50,6 +51,7 @@ application.register('per-page', PerPageController)
|
|
50
51
|
application.register('search', SearchController)
|
51
52
|
application.register('select', SelectController)
|
52
53
|
application.register('select-filter', SelectFilterController)
|
54
|
+
application.register('tags-field', TagsFieldController)
|
53
55
|
application.register('text-filter', TextFilterController)
|
54
56
|
application.register('tippy', TippyController)
|
55
57
|
application.register('toggle-panel', TogglePanelController)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
<div class="flex mt-4 items-end">
|
2
2
|
<span class="text-3xl"><%= @card.prefix %></span>
|
3
|
-
<span class="text-5xl"><%= @card.result_data %></span>
|
3
|
+
<span class="text-5xl"><%= @card.compute_result.result_data %></span>
|
4
4
|
<span class="text-3xl"><%= @card.suffix %></span>
|
5
5
|
</div>
|
data/db/factories.rb
CHANGED
@@ -25,6 +25,7 @@ FactoryBot.define do
|
|
25
25
|
Time.now - rand(10...365).days
|
26
26
|
end
|
27
27
|
end
|
28
|
+
tag_list { ["1", "2", "five", "seven"].shuffle }
|
28
29
|
status { ::Post.statuses.keys.sample }
|
29
30
|
end
|
30
31
|
|
@@ -64,6 +65,7 @@ FactoryBot.define do
|
|
64
65
|
|
65
66
|
factory :course do
|
66
67
|
name { Faker::Educator.unique.course_name }
|
68
|
+
skills { [Faker::Educator.subject, Faker::Educator.subject, Faker::Educator.subject, Faker::Educator.subject, Faker::Educator.subject] }
|
67
69
|
country { Course.countries.sample }
|
68
70
|
city { Course.cities.stringify_keys[country].sample }
|
69
71
|
end
|
data/lib/avo/base_resource.rb
CHANGED
@@ -0,0 +1,36 @@
|
|
1
|
+
module Avo
|
2
|
+
module Concerns
|
3
|
+
module HandlesFieldArgs
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
# Add an instance variable from args
|
9
|
+
# That may be a string, boolean, or array
|
10
|
+
# Each args should also have a default value
|
11
|
+
def add_prop_from_args(args = {}, name: nil, type: :string, default: nil)
|
12
|
+
value = default
|
13
|
+
|
14
|
+
if type == :boolean
|
15
|
+
value = args[name.to_sym] == true
|
16
|
+
else
|
17
|
+
value = args[name.to_sym] unless args.dig(name.to_sym).nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
instance_variable_set(:"@#{name}", value)
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_boolean_prop(args, name, default = false)
|
24
|
+
add_prop_from_args args, name: name, default: default, type: :boolean
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_array_prop(args, name, default = [])
|
28
|
+
add_prop_from_args args, name: name, default: default, type: :array
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_string_prop(args, name, default = [])
|
32
|
+
add_prop_from_args args, name: name, default: default, type: :string
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -4,8 +4,9 @@ module Avo
|
|
4
4
|
extend ActiveSupport::DescendantsTracker
|
5
5
|
extend Avo::Fields::FieldExtensions::HasFieldName
|
6
6
|
|
7
|
-
include ActionView::Helpers::UrlHelper
|
8
7
|
include Avo::Fields::FieldExtensions::VisibleInDifferentViews
|
8
|
+
include Avo::Concerns::HandlesFieldArgs
|
9
|
+
include ActionView::Helpers::UrlHelper
|
9
10
|
|
10
11
|
delegate :view_context, to: "Avo::App"
|
11
12
|
delegate :main_app, to: :view_context
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Avo
|
2
|
+
module Fields
|
3
|
+
class TagsField < BaseField
|
4
|
+
attr_reader :acts_as_taggable_on
|
5
|
+
attr_reader :close_on_select
|
6
|
+
attr_reader :delimiters
|
7
|
+
attr_reader :enforce_suggestions
|
8
|
+
|
9
|
+
def initialize(id, **args, &block)
|
10
|
+
super(id, **args, &block)
|
11
|
+
|
12
|
+
add_boolean_prop args, :close_on_select
|
13
|
+
add_boolean_prop args, :enforce_suggestions
|
14
|
+
add_string_prop args, :acts_as_taggable_on
|
15
|
+
add_array_prop args, :blacklist
|
16
|
+
add_array_prop args, :delimiters, [","]
|
17
|
+
add_array_prop args, :suggestions
|
18
|
+
end
|
19
|
+
|
20
|
+
def field_value
|
21
|
+
return json_value if acts_as_taggable_on.present?
|
22
|
+
|
23
|
+
value || []
|
24
|
+
end
|
25
|
+
|
26
|
+
def json_value
|
27
|
+
value.map do |item|
|
28
|
+
{
|
29
|
+
value: item.name
|
30
|
+
}
|
31
|
+
end.as_json
|
32
|
+
end
|
33
|
+
|
34
|
+
def fill_field(model, key, value, params)
|
35
|
+
if acts_as_taggable_on.present?
|
36
|
+
model.send(act_as_taggable_attribute(key), parsed_value(value))
|
37
|
+
else
|
38
|
+
model.send("#{key}=", parsed_value(value))
|
39
|
+
end
|
40
|
+
|
41
|
+
model
|
42
|
+
end
|
43
|
+
|
44
|
+
def suggestions
|
45
|
+
return @suggestions if @suggestions.is_a? Array
|
46
|
+
|
47
|
+
if @suggestions.respond_to? :call
|
48
|
+
return Avo::Hosts::RecordHost.new(block: @suggestions, record: model).handle
|
49
|
+
end
|
50
|
+
|
51
|
+
[]
|
52
|
+
end
|
53
|
+
|
54
|
+
def blacklist
|
55
|
+
return @blacklist if @blacklist.is_a? Array
|
56
|
+
|
57
|
+
if @blacklist.respond_to? :call
|
58
|
+
return Avo::Hosts::RecordHost.new(block: @blacklist, record: model).handle
|
59
|
+
end
|
60
|
+
|
61
|
+
[]
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def act_as_taggable_attribute(key)
|
67
|
+
"#{key.singularize}_list="
|
68
|
+
end
|
69
|
+
|
70
|
+
def parsed_value(value)
|
71
|
+
JSON.parse(value).pluck("value")
|
72
|
+
rescue
|
73
|
+
[]
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def parse_suggestions_from_args(args)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/avo/version.rb
CHANGED
@@ -9,7 +9,7 @@ class <%= class_name.camelize %> < Avo::Dashboards::ChartkickCard
|
|
9
9
|
# self.chart_options = { library: { plugins: { legend: { display: true } } } }
|
10
10
|
# self.flush = true
|
11
11
|
|
12
|
-
|
12
|
+
query do
|
13
13
|
points = 16
|
14
14
|
i = Time.new.year.to_i - points
|
15
15
|
base_data = Array.new(points).map do
|