active_element 0.0.1 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +12 -0
- data/.strong_versions.yml +2 -0
- data/Gemfile +10 -2
- data/Gemfile.lock +229 -4
- data/Rakefile +1 -0
- data/active_element.gemspec +7 -0
- data/app/assets/config/active_element/manifest.js +2 -0
- data/app/assets/javascripts/active_element/application.js +10 -0
- data/app/assets/javascripts/active_element/confirm.js +67 -0
- data/app/assets/javascripts/active_element/form.js +61 -0
- data/app/assets/javascripts/active_element/json_field.js +316 -0
- data/app/assets/javascripts/active_element/pagination.js +18 -0
- data/app/assets/javascripts/active_element/search_field.js +127 -0
- data/app/assets/javascripts/active_element/secret.js +40 -0
- data/app/assets/javascripts/active_element/setup.js +36 -0
- data/app/assets/javascripts/active_element/theme.js +42 -0
- data/app/assets/stylesheets/active_element/_variables.scss +142 -0
- data/app/assets/stylesheets/active_element/application.scss +77 -0
- data/app/controllers/active_element/application_controller.rb +41 -0
- data/app/controllers/active_element/text_searches_controller.rb +189 -0
- data/app/views/active_element/components/_horizontal_tabs.html.erb +32 -0
- data/app/views/active_element/components/_vertical_tabs.html.erb +38 -0
- data/app/views/active_element/components/button.html.erb +27 -0
- data/app/views/active_element/components/fields/_boolean.html.erb +11 -0
- data/app/views/active_element/components/form/_check_box.html.erb +3 -0
- data/app/views/active_element/components/form/_check_boxes.html.erb +33 -0
- data/app/views/active_element/components/form/_field.html.erb +28 -0
- data/app/views/active_element/components/form/_generic_field.html.erb +3 -0
- data/app/views/active_element/components/form/_json.html.erb +12 -0
- data/app/views/active_element/components/form/_label.html.erb +17 -0
- data/app/views/active_element/components/form/_option_groups_summary.html.erb +17 -0
- data/app/views/active_element/components/form/_select.html.erb +4 -0
- data/app/views/active_element/components/form/_summary.html.erb +40 -0
- data/app/views/active_element/components/form/_templates.html.erb +85 -0
- data/app/views/active_element/components/form/_text_area.html.erb +4 -0
- data/app/views/active_element/components/form/_text_search.html.erb +16 -0
- data/app/views/active_element/components/form.html.erb +78 -0
- data/app/views/active_element/components/json.html.erb +8 -0
- data/app/views/active_element/components/page_description.html.erb +3 -0
- data/app/views/active_element/components/secret/_field.html.erb +1 -0
- data/app/views/active_element/components/secret/_templates.html.erb +11 -0
- data/app/views/active_element/components/table/_collection_row.html.erb +30 -0
- data/app/views/active_element/components/table/_grouped_collection.html.erb +88 -0
- data/app/views/active_element/components/table/_pagination.html.erb +17 -0
- data/app/views/active_element/components/table/_ungrouped_collection.html.erb +49 -0
- data/app/views/active_element/components/table/collection.html.erb +39 -0
- data/app/views/active_element/components/table/item.html.erb +39 -0
- data/app/views/active_element/components/tabs.html.erb +7 -0
- data/app/views/active_element/decorators/_boolean.html.erb +5 -0
- data/app/views/active_element/decorators/_date.html.erb +3 -0
- data/app/views/active_element/decorators/_datetime.html.erb +3 -0
- data/app/views/active_element/decorators/_time.html.erb +3 -0
- data/app/views/active_element/forbidden.html.erb +33 -0
- data/app/views/active_element/main_menu/_item.html.erb +9 -0
- data/app/views/active_element/navbar/_menu.html.erb +30 -0
- data/app/views/active_element/theme/_select.html.erb +1 -0
- data/app/views/active_element/theme/_templates.html.erb +6 -0
- data/app/views/kaminari/_first_page.html.erb +3 -0
- data/app/views/kaminari/_gap.html.erb +3 -0
- data/app/views/kaminari/_last_page.html.erb +3 -0
- data/app/views/kaminari/_next_page.html.erb +3 -0
- data/app/views/kaminari/_page.html.erb +9 -0
- data/app/views/kaminari/_paginator.html.erb +17 -0
- data/app/views/kaminari/_prev_page.html.erb +3 -0
- data/app/views/layouts/active_element.html.erb +65 -0
- data/app/views/layouts/active_element_error.html.erb +40 -0
- data/config/routes.rb +5 -0
- data/lib/active_element/active_menu_link.rb +80 -0
- data/lib/active_element/active_record_text_search_authorization.rb +12 -0
- data/lib/active_element/colorized_string.rb +33 -0
- data/lib/active_element/component.rb +122 -0
- data/lib/active_element/components/button.rb +156 -0
- data/lib/active_element/components/collection_table.rb +118 -0
- data/lib/active_element/components/form.rb +210 -0
- data/lib/active_element/components/item_table.rb +57 -0
- data/lib/active_element/components/json.rb +31 -0
- data/lib/active_element/components/link_helpers.rb +9 -0
- data/lib/active_element/components/page_description.rb +28 -0
- data/lib/active_element/components/secret_fields.rb +15 -0
- data/lib/active_element/components/tab.rb +37 -0
- data/lib/active_element/components/tabs.rb +35 -0
- data/lib/active_element/components/translations.rb +18 -0
- data/lib/active_element/components/util/association_mapping.rb +80 -0
- data/lib/active_element/components/util/decorator.rb +107 -0
- data/lib/active_element/components/util/display_value_mapping.rb +48 -0
- data/lib/active_element/components/util/field_mapping.rb +144 -0
- data/lib/active_element/components/util/form_field_mapping.rb +104 -0
- data/lib/active_element/components/util/form_value_mapping.rb +49 -0
- data/lib/active_element/components/util/i18n.rb +66 -0
- data/lib/active_element/components/util/record_mapping.rb +111 -0
- data/lib/active_element/components/util.rb +43 -0
- data/lib/active_element/components.rb +20 -0
- data/lib/active_element/controller_action.rb +91 -0
- data/lib/active_element/engine.rb +26 -0
- data/lib/active_element/permissions_check.rb +101 -0
- data/lib/active_element/rails_component.rb +40 -0
- data/lib/active_element/route.rb +112 -0
- data/lib/active_element/routes.rb +62 -0
- data/lib/active_element/version.rb +1 -1
- data/lib/active_element.rb +91 -1
- data/lib/tasks/active_element.rake +23 -0
- data/rspec-documentation/dummy +1 -0
- data/rspec-documentation/pages/Components/Forms.md +1 -0
- data/rspec-documentation/pages/Components/Tables.md +47 -0
- data/rspec-documentation/pages/Components/Tabs.md +1 -0
- data/rspec-documentation/pages/Components.md +1 -0
- data/rspec-documentation/pages/Decorators/Inline Decorators.md +1 -0
- data/rspec-documentation/pages/Decorators/View Decorators.md +1 -0
- data/rspec-documentation/pages/Index.md +3 -0
- data/rspec-documentation/pages/Util/I18n.md +1 -0
- data/rspec-documentation/spec_helper.rb +35 -0
- metadata +191 -3
@@ -0,0 +1,9 @@
|
|
1
|
+
<% if page.current? %>
|
2
|
+
<li class="page-item active">
|
3
|
+
<%= content_tag :a, page, data: { remote: remote }, rel: page.rel, class: 'page-link' %>
|
4
|
+
</li>
|
5
|
+
<% else %>
|
6
|
+
<li class="page-item">
|
7
|
+
<%= link_to page, url, remote: remote, rel: page.rel, class: 'page-link' %>
|
8
|
+
</li>
|
9
|
+
<% end %>
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<%= paginator.render do %>
|
2
|
+
<nav>
|
3
|
+
<ul class="pagination">
|
4
|
+
<%= first_page_tag unless current_page.first? %>
|
5
|
+
<%= prev_page_tag unless current_page.first? %>
|
6
|
+
<% each_page do |page| %>
|
7
|
+
<% if page.left_outer? || page.right_outer? || page.inside_window? %>
|
8
|
+
<%= page_tag page %>
|
9
|
+
<% elsif !page.was_truncated? -%>
|
10
|
+
<%= gap_tag %>
|
11
|
+
<% end %>
|
12
|
+
<% end %>
|
13
|
+
<%= next_page_tag unless current_page.last? %>
|
14
|
+
<%= last_page_tag unless current_page.last? %>
|
15
|
+
</ul>
|
16
|
+
</nav>
|
17
|
+
<% end %>
|
@@ -0,0 +1,65 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
<%= render_active_element_hook 'active_element/before_head' %>
|
4
|
+
|
5
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
6
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js" integrity="sha512-fD9DI5bZwQxOi7MhYWnnNPlvXdp/2Pj3XSTRrFs5FQa4mizyGLnJcN6tuvUS6LbmgN1ut+XGSABKvjN0H6Aoow==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
7
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js" integrity="sha512-2rNj2KJ+D8s1ceNasTIex6z4HWyOnEYLVC3FigGOmyQCZc2eBXKgOxQmo3oKLHyfcj53uz4QMsRCWNbLd32Q1g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
8
|
+
|
9
|
+
<%= stylesheet_link_tag 'active_element/application', 'data-turbolinks-track': 'reload' %>
|
10
|
+
<%= javascript_include_tag 'active_element/application', 'data-turbolinks-track': 'reload' %>
|
11
|
+
|
12
|
+
<% if Rails.application.assets.find_asset('application.css').present? %>
|
13
|
+
<%= stylesheet_link_tag 'application', 'data-turbolinks-track': 'reload' %>
|
14
|
+
<% end %>
|
15
|
+
|
16
|
+
<% if Rails.application.assets.find_asset('application.js').present? %>
|
17
|
+
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
|
18
|
+
<% end %>
|
19
|
+
|
20
|
+
<%= csrf_meta_tag %>
|
21
|
+
|
22
|
+
<%= render_active_element_hook 'active_element/after_head' %>
|
23
|
+
</head>
|
24
|
+
|
25
|
+
<body>
|
26
|
+
<%= render_active_element_hook 'active_element/before_navbar' %>
|
27
|
+
<%= render partial: 'active_element/navbar/menu' %>
|
28
|
+
<%= render_active_element_hook 'active_element/after_navbar' %>
|
29
|
+
|
30
|
+
<% flash.each do |type, message| %>
|
31
|
+
<div class="flash card text-bg-<%= { notice: 'info', alert: 'danger' }.fetch(type.to_sym, 'info') %> flash <%= type %> float-end">
|
32
|
+
<div class="card-body">
|
33
|
+
<%= message %>
|
34
|
+
<button type="button" class="btn-close" aria-label="Close"></button>
|
35
|
+
</div>
|
36
|
+
</div>
|
37
|
+
<% end %>
|
38
|
+
|
39
|
+
<%= render_active_element_hook 'active_element/before_content' %>
|
40
|
+
|
41
|
+
<div class="content p-3 m-3">
|
42
|
+
<%= yield %>
|
43
|
+
</div>
|
44
|
+
|
45
|
+
<%= render partial: 'active_element/components/form/templates' %>
|
46
|
+
<%= render partial: 'active_element/components/secret/templates' %>
|
47
|
+
<%= render partial: 'active_element/theme/templates' %>
|
48
|
+
|
49
|
+
<%= render_active_element_hook 'active_element/after_content' %>
|
50
|
+
|
51
|
+
<script>
|
52
|
+
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'))
|
53
|
+
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
|
54
|
+
return new bootstrap.Popover(popoverTriggerEl)
|
55
|
+
})
|
56
|
+
|
57
|
+
document.querySelectorAll('.flash .btn-close').forEach(element => {
|
58
|
+
element.onclick = ev => {
|
59
|
+
ev.stopPropagation();
|
60
|
+
ev.target.parentElement.parentElement.classList.add('d-none');
|
61
|
+
};
|
62
|
+
});
|
63
|
+
</script>
|
64
|
+
</body>
|
65
|
+
</html>
|
@@ -0,0 +1,40 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
|
4
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
5
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script>
|
6
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js" integrity="sha512-fD9DI5bZwQxOi7MhYWnnNPlvXdp/2Pj3XSTRrFs5FQa4mizyGLnJcN6tuvUS6LbmgN1ut+XGSABKvjN0H6Aoow==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
7
|
+
</head>
|
8
|
+
|
9
|
+
<style>
|
10
|
+
body {
|
11
|
+
background-color: #623131;
|
12
|
+
}
|
13
|
+
|
14
|
+
a {
|
15
|
+
color: #d7df93;
|
16
|
+
}
|
17
|
+
|
18
|
+
div.content {
|
19
|
+
position: absolute;
|
20
|
+
color: #efefef;
|
21
|
+
left: 50%;
|
22
|
+
top: 40%;
|
23
|
+
-webkit-transform: translate(-50%, -50%);
|
24
|
+
transform: translate(-50%, -50%);
|
25
|
+
text-align: center;
|
26
|
+
}
|
27
|
+
</style>
|
28
|
+
|
29
|
+
<%= render_active_element_hook 'active_element/before_error_content' %>
|
30
|
+
|
31
|
+
<body>
|
32
|
+
<div class="wrapper">
|
33
|
+
<div class="middle">
|
34
|
+
<div class="content p-3 m-3">
|
35
|
+
<%= yield %>
|
36
|
+
</div>
|
37
|
+
</div>
|
38
|
+
</div>
|
39
|
+
</body>
|
40
|
+
</html>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveElement
|
4
|
+
# Detects which link should be highlighted in the main application menu.
|
5
|
+
class ActiveMenuLink
|
6
|
+
# rubocop:disable Metrics/ParameterLists
|
7
|
+
def initialize(rails_component:, current_path:, controller_path:, action_name:, current_navbar_item:, navbar_items:)
|
8
|
+
@rails_component = rails_component
|
9
|
+
@current_path = current_path
|
10
|
+
@controller_path = controller_path
|
11
|
+
@action_name = action_name
|
12
|
+
@current_navbar_item = current_navbar_item
|
13
|
+
@navbar_items = navbar_items
|
14
|
+
end
|
15
|
+
# rubocop:enable Metrics/ParameterLists
|
16
|
+
|
17
|
+
def active?
|
18
|
+
return true if exact_match?
|
19
|
+
return true if !any_exact_match? && exact_without_query_string_match?
|
20
|
+
return true if !any_exact_match? && !any_exact_without_query_string_match? && exact_without_resource_match?
|
21
|
+
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :rails_component,
|
28
|
+
:current_path, :controller_path, :action_name,
|
29
|
+
:current_navbar_item, :navbar_items
|
30
|
+
|
31
|
+
def exact_match?(navbar_item = current_navbar_item)
|
32
|
+
exact_matched_route(navbar_item)&.fetch(:path) == current_path
|
33
|
+
end
|
34
|
+
|
35
|
+
def exact_matched_route(navbar_item)
|
36
|
+
rails_component.route_paths_with_requirements.find do |route_path_with_requirements|
|
37
|
+
route_path_with_requirements[:path] == navbar_item[:path]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def any_exact_match?
|
42
|
+
navbar_items.any? do |navbar_item|
|
43
|
+
exact_match?(navbar_item)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def exact_without_query_string_match?(navbar_item = current_navbar_item)
|
48
|
+
exact_matched_route_without_query_string(navbar_item)&.fetch(:path) == without_query_string(current_path)
|
49
|
+
end
|
50
|
+
|
51
|
+
def any_exact_without_query_string_match?
|
52
|
+
navbar_items.any? do |navbar_item|
|
53
|
+
exact_without_query_string_match?(navbar_item)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def exact_matched_route_without_query_string(navbar_item)
|
58
|
+
rails_component.route_paths_with_requirements.find do |route_path_with_requirements|
|
59
|
+
route_path_with_requirements[:path] == without_query_string(navbar_item[:path])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def without_query_string(path)
|
64
|
+
path.rpartition('?').compact_blank.first
|
65
|
+
end
|
66
|
+
|
67
|
+
def exact_without_resource_match?
|
68
|
+
route = exact_matched_route_without_resource(current_navbar_item)
|
69
|
+
return false if route.blank?
|
70
|
+
|
71
|
+
route[:controller] == controller_path
|
72
|
+
end
|
73
|
+
|
74
|
+
def exact_matched_route_without_resource(navbar_item)
|
75
|
+
rails_component.route_paths_with_requirements.find do |route_path_with_requirements|
|
76
|
+
navbar_item.dig(:spec, :controller) == route_path_with_requirements[:controller]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
ActiveRecord::Base.class_eval do
|
4
|
+
class << self
|
5
|
+
attr_reader :authorized_active_element_text_search_fields
|
6
|
+
|
7
|
+
def authorize_active_element_text_search_for(field, exposes:)
|
8
|
+
@authorized_active_element_text_search_fields ||= []
|
9
|
+
@authorized_active_element_text_search_fields << [field, exposes]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveElement
|
4
|
+
# Wraps strings in terminal escape codes to provide colourisation in (e.g.) logging output.
|
5
|
+
class ColorizedString
|
6
|
+
COLOR_CODES = {
|
7
|
+
cyan: '36',
|
8
|
+
red: '31',
|
9
|
+
green: '32',
|
10
|
+
blue: '34',
|
11
|
+
purple: '35',
|
12
|
+
yellow: '33',
|
13
|
+
light_gray: '37',
|
14
|
+
light_blue: '1;34',
|
15
|
+
white: '1;37'
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
def initialize(string, color:)
|
19
|
+
@string = string
|
20
|
+
@color = color
|
21
|
+
end
|
22
|
+
|
23
|
+
def value
|
24
|
+
return string unless Rails.env.development? || Rails.env.test?
|
25
|
+
|
26
|
+
"\e[#{COLOR_CODES.fetch(color)}m#{string}\e[0m"
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :string, :color
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveElement
|
4
|
+
# Exposed by `component` view helper, used as general entrypoint for component creation.
|
5
|
+
class Component
|
6
|
+
def initialize(controller)
|
7
|
+
@controller = controller
|
8
|
+
end
|
9
|
+
|
10
|
+
def page_title(title, **kwargs)
|
11
|
+
controller.content_tag(:h2, title, **kwargs)
|
12
|
+
end
|
13
|
+
|
14
|
+
def page_subtitle(subtitle, **kwargs)
|
15
|
+
controller.content_tag(:h3, subtitle, **kwargs)
|
16
|
+
end
|
17
|
+
|
18
|
+
def page_section_title(section_title, **kwargs)
|
19
|
+
kwargs[:class] ||= 'mt-3'
|
20
|
+
controller.content_tag(:h4, section_title, **kwargs)
|
21
|
+
end
|
22
|
+
|
23
|
+
def page_description(content)
|
24
|
+
render Components::PageDescription.new(controller, content: content)
|
25
|
+
end
|
26
|
+
|
27
|
+
def show_button(record = nil, flag_or_options = true, **kwargs) # rubocop:disable Style/OptionalBooleanParameter
|
28
|
+
render Components::Button.new(controller, record, flag_or_options, type: :show, **kwargs)
|
29
|
+
end
|
30
|
+
|
31
|
+
def new_button(record = nil, flag_or_options = true, **kwargs) # rubocop:disable Style/OptionalBooleanParameter
|
32
|
+
render Components::Button.new(controller, record, flag_or_options, type: :new, **kwargs)
|
33
|
+
end
|
34
|
+
|
35
|
+
def edit_button(record = nil, flag_or_options = true, **kwargs) # rubocop:disable Style/OptionalBooleanParameter
|
36
|
+
render Components::Button.new(controller, record, flag_or_options, type: :edit, **kwargs)
|
37
|
+
end
|
38
|
+
|
39
|
+
def destroy_button(record = nil, flag_or_options = true, **kwargs) # rubocop:disable Style/OptionalBooleanParameter
|
40
|
+
confirm = kwargs.delete(:confirm) { true }
|
41
|
+
|
42
|
+
render Components::Button.new(
|
43
|
+
controller, record, flag_or_options, type: :destroy, confirm: confirm, **kwargs
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
def button(title = nil, url = nil, type: 'primary', float: nil, **kwargs, &block)
|
48
|
+
render Components::Button.new(
|
49
|
+
controller, nil, { path: url, title: title }, type: type, float: float, **kwargs, &block
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
def json(key, object)
|
54
|
+
render Components::Json.new(controller, object: object, key: key)
|
55
|
+
end
|
56
|
+
|
57
|
+
def tabs(class: nil, &block)
|
58
|
+
class_name = binding.local_variable_get(:class) || 'tabs'
|
59
|
+
render Components::Tabs.new(controller, class_name: class_name, &block)
|
60
|
+
end
|
61
|
+
|
62
|
+
def table(**kwargs)
|
63
|
+
class_name = kwargs.delete(:class) { default_class(kwargs[:collection], kwargs[:item], kwargs[:model_name]) }
|
64
|
+
|
65
|
+
return render item_table(controller, class_name: class_name, **kwargs) if kwargs.key?(:item)
|
66
|
+
|
67
|
+
if kwargs.key?(:collection)
|
68
|
+
return render collection_table(controller, class_name: class_name, params: params, **kwargs)
|
69
|
+
end
|
70
|
+
|
71
|
+
raise ArgumentError, 'Must provide one of `item` or `collection`.'
|
72
|
+
end
|
73
|
+
|
74
|
+
def form(fields: nil, submit: nil, item: nil, **kwargs)
|
75
|
+
render Components::Form.new(controller, fields: fields, submit: submit, item: item, **kwargs)
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
attr_reader :controller
|
81
|
+
|
82
|
+
def item_table(controller, **kwargs)
|
83
|
+
Components::ItemTable.new(controller, **kwargs)
|
84
|
+
end
|
85
|
+
|
86
|
+
def collection_table(controller, **kwargs)
|
87
|
+
Components::CollectionTable.new(controller, **kwargs)
|
88
|
+
end
|
89
|
+
|
90
|
+
def render(component)
|
91
|
+
ActiveElement.with_silenced_logging do
|
92
|
+
controller.render_to_string component.template, locals: component.locals, layout: nil
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def default_class(collection, item, model_name)
|
97
|
+
class_from_collection(collection, model_name) || class_from_item(item, model_name)
|
98
|
+
end
|
99
|
+
|
100
|
+
def class_from_collection(collection, model_name)
|
101
|
+
return model_name.pluralize if model_name.present?
|
102
|
+
return nil if collection.nil?
|
103
|
+
|
104
|
+
Components::Util::I18n.class_name(class_name_from_item(collection.first), plural: true)
|
105
|
+
end
|
106
|
+
|
107
|
+
def class_from_item(item, model_name)
|
108
|
+
return model_name.pluralize if model_name.present?
|
109
|
+
return nil if item.nil?
|
110
|
+
|
111
|
+
Components::Util::I18n.class_name(class_name_from_item(item))
|
112
|
+
end
|
113
|
+
|
114
|
+
def params
|
115
|
+
controller.params
|
116
|
+
end
|
117
|
+
|
118
|
+
def class_name_from_item(item)
|
119
|
+
Components::Util.sti_record_name(item) || Components::Util.record_name(item)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveElement
|
4
|
+
module Components
|
5
|
+
# A clickable button.
|
6
|
+
# rubocop:disable Metrics/ClassLength
|
7
|
+
class Button
|
8
|
+
# rubocop:disable Metrics/MethodLength
|
9
|
+
def initialize(controller, record, flag_or_options, confirm: false, type: :primary, method: nil,
|
10
|
+
float: nil, icon: nil, **kwargs, &block)
|
11
|
+
@controller = controller
|
12
|
+
@record = record.is_a?(ActiveRecord::Relation) ? record.klass.new : record
|
13
|
+
@flag_or_options = flag_or_options
|
14
|
+
@float = float
|
15
|
+
@kwargs = kwargs
|
16
|
+
@kwargs_class = kwargs.delete(:class)
|
17
|
+
@confirm = confirm
|
18
|
+
@type = type
|
19
|
+
@method = method
|
20
|
+
@icon = icon
|
21
|
+
@block_given = block_given?
|
22
|
+
@content = block.call if block_given?
|
23
|
+
end
|
24
|
+
# rubocop:enable Metrics/MethodLength
|
25
|
+
|
26
|
+
def template
|
27
|
+
'active_element/components/button'
|
28
|
+
end
|
29
|
+
|
30
|
+
def locals # rubocop:disable Metrics/MethodLength
|
31
|
+
{
|
32
|
+
controller: controller,
|
33
|
+
method: link_method,
|
34
|
+
path: resource_path,
|
35
|
+
confirm: confirm,
|
36
|
+
type: type,
|
37
|
+
title: title,
|
38
|
+
float_class: float_class,
|
39
|
+
icon: icon,
|
40
|
+
button_class: button_class,
|
41
|
+
kwargs_class: kwargs_class,
|
42
|
+
kwargs: kwargs,
|
43
|
+
content: content,
|
44
|
+
block_given: block_given
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
attr_reader :controller, :record, :flag_or_options, :float, :kwargs, :kwargs_class, :type, :method, :icon,
|
51
|
+
:block_given, :content, :confirm
|
52
|
+
|
53
|
+
def link_method
|
54
|
+
return method if method.present?
|
55
|
+
|
56
|
+
{ show: :get, destroy: :delete, new: :get, edit: :get }.fetch(type, :get)
|
57
|
+
end
|
58
|
+
|
59
|
+
def button_class # rubocop:disable Metrics/MethodLength
|
60
|
+
case type
|
61
|
+
when :destroy
|
62
|
+
'btn-danger destroy-button action-button'
|
63
|
+
when :show
|
64
|
+
'btn-primary show-button action-button'
|
65
|
+
when :edit
|
66
|
+
'btn-primary edit-button action-button'
|
67
|
+
when :new
|
68
|
+
'btn-primary new-button action-button'
|
69
|
+
else
|
70
|
+
"btn-#{type}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def float_class
|
75
|
+
{ 'end' => 'float-end', 'start' => 'float-start', nil => nil }.fetch(float)
|
76
|
+
end
|
77
|
+
|
78
|
+
def namespace_prefix
|
79
|
+
# XXX: We guess the namespace from the current controller's module name. This will work
|
80
|
+
# most of the time but will break the current record's controller exists in a different
|
81
|
+
# namespace to the current controller, e.g. `BackEndAdmin::UsersController` and
|
82
|
+
# `FrontEndAdmin::ThemesController` - if `FrontEndAdmin::ThemesController` renders a
|
83
|
+
# collection of `User` objects, the "show" path will be wrong:
|
84
|
+
# `front_end_admin_user_path`. Maybe descend through the full controller class tree to
|
85
|
+
# find a best match ?
|
86
|
+
namespace = controller.class.name.deconstantize.underscore
|
87
|
+
return nil if namespace.blank?
|
88
|
+
|
89
|
+
"#{namespace}_"
|
90
|
+
end
|
91
|
+
|
92
|
+
def record_path
|
93
|
+
return nil if record.nil?
|
94
|
+
|
95
|
+
controller.helpers.public_send(default_record_path, record)
|
96
|
+
rescue NoMethodError
|
97
|
+
controller.helpers.public_send(sti_record_path, record)
|
98
|
+
end
|
99
|
+
|
100
|
+
def default_record_path
|
101
|
+
"#{record_path_prefix}#{namespace_prefix}#{record_name}_path"
|
102
|
+
end
|
103
|
+
|
104
|
+
def sti_record_path
|
105
|
+
"#{record_path_prefix}#{namespace_prefix}#{sti_record_name}_path"
|
106
|
+
end
|
107
|
+
|
108
|
+
def record_path_prefix
|
109
|
+
case type
|
110
|
+
when :edit
|
111
|
+
'edit_'
|
112
|
+
when :new
|
113
|
+
'new_'
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def title
|
118
|
+
return flag_or_options[:title] if flag_or_options.is_a?(Hash) && flag_or_options[:title].present?
|
119
|
+
return default_action_title if %i[show destroy edit new].include?(type)
|
120
|
+
|
121
|
+
kwargs.fetch(:title, '')
|
122
|
+
end
|
123
|
+
|
124
|
+
def default_action_title
|
125
|
+
return 'View' if type == :show
|
126
|
+
return 'Delete' if type == :destroy
|
127
|
+
return 'Edit' if type == :edit
|
128
|
+
return "Create New #{(sti_record_name || record_name).titleize}" if type == :new
|
129
|
+
end
|
130
|
+
|
131
|
+
def resource_path
|
132
|
+
return record_path if flag_or_options == true && record.present?
|
133
|
+
return nil if !flag_or_options.is_a?(Hash) && record.blank?
|
134
|
+
return resource_path_from_options if resource_path_from_options.present?
|
135
|
+
|
136
|
+
record_path
|
137
|
+
end
|
138
|
+
|
139
|
+
def resource_path_from_options
|
140
|
+
return nil unless flag_or_options.is_a?(Hash)
|
141
|
+
return flag_or_options[:path].call(record) if flag_or_options[:path].is_a?(Proc)
|
142
|
+
|
143
|
+
flag_or_options[:path]
|
144
|
+
end
|
145
|
+
|
146
|
+
def record_name
|
147
|
+
Util.record_name(record)
|
148
|
+
end
|
149
|
+
|
150
|
+
def sti_record_name
|
151
|
+
Util.sti_record_name(record)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
# rubocop:enable Metrics/ClassLength
|
156
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveElement
|
4
|
+
module Components
|
5
|
+
# A table component for rendering a collection of data.
|
6
|
+
class CollectionTable
|
7
|
+
include LinkHelpers
|
8
|
+
include SecretFields
|
9
|
+
include Translations
|
10
|
+
|
11
|
+
DEFAULT_PAGE_SIZE = 50
|
12
|
+
|
13
|
+
attr_reader :controller, :model_name
|
14
|
+
|
15
|
+
# rubocop:disable Metrics/MethodLength
|
16
|
+
def initialize(controller, class_name:, collection:, fields:, params:, model_name: nil, style: nil,
|
17
|
+
show: false, new: false, edit: false, destroy: false, paginate: true, group: nil,
|
18
|
+
group_title: false, row_class: nil, **_kwargs)
|
19
|
+
@controller = controller
|
20
|
+
@class_name = class_name
|
21
|
+
@model_name = model_name
|
22
|
+
@collection = collection || []
|
23
|
+
@fields = fields
|
24
|
+
@style = style
|
25
|
+
@params = params
|
26
|
+
@show = show
|
27
|
+
@new = new
|
28
|
+
@edit = edit
|
29
|
+
@destroy = destroy
|
30
|
+
@paginate = paginate
|
31
|
+
@group = group
|
32
|
+
@group_title = group_title
|
33
|
+
@row_class = row_class
|
34
|
+
verify_paginate_and_group
|
35
|
+
end
|
36
|
+
# rubocop:enable Metrics/MethodLength
|
37
|
+
|
38
|
+
def template
|
39
|
+
'active_element/components/table/collection'
|
40
|
+
end
|
41
|
+
|
42
|
+
def locals # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
43
|
+
{
|
44
|
+
component: self,
|
45
|
+
class_name: class_name,
|
46
|
+
collection: group ? collection : paginated_collection,
|
47
|
+
fields: Util::FieldMapping.new(self, fields, class_name).mapped_fields,
|
48
|
+
style: style,
|
49
|
+
new: new,
|
50
|
+
show: show,
|
51
|
+
edit: edit,
|
52
|
+
destroy: destroy,
|
53
|
+
group: group,
|
54
|
+
group_title: group_title,
|
55
|
+
display_pagination: display_pagination?,
|
56
|
+
page_sizes: [5, 10, 25, 50, 75, 100, 200],
|
57
|
+
page_size: page_size,
|
58
|
+
i18n: i18n,
|
59
|
+
row_class_mapper: row_class_mapper
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
def model
|
64
|
+
return collection.klass if collection.is_a?(ActiveRecord::Relation)
|
65
|
+
|
66
|
+
collection&.first.class.is_a?(ActiveModel::Naming) ? collection.first.class : nil
|
67
|
+
end
|
68
|
+
|
69
|
+
def grouped_collection
|
70
|
+
collection.group_by do |item|
|
71
|
+
item.class.is_a?(ActiveModel::Naming) ? item.public_send(group) : item.fetch(group)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
attr_reader :class_name, :collection, :fields, :style, :row_class,
|
78
|
+
:new, :show, :edit, :destroy,
|
79
|
+
:paginate, :params, :group, :group_title
|
80
|
+
|
81
|
+
def paginated_collection
|
82
|
+
return collection unless paginate && collection.respond_to?(:page)
|
83
|
+
return collection.page(page_number).per(page_size) if supports_pagination_but_not_yet_paginated?
|
84
|
+
|
85
|
+
@paginated_collection ||= collection.page(page_number).per(page_size)
|
86
|
+
end
|
87
|
+
|
88
|
+
def supports_pagination_but_not_yet_paginated?
|
89
|
+
collection.respond_to?(:page) && !collection.respond_to?(:current_per_page)
|
90
|
+
end
|
91
|
+
|
92
|
+
def page_number
|
93
|
+
params[:page].presence || 1
|
94
|
+
end
|
95
|
+
|
96
|
+
def page_size
|
97
|
+
params[:page_size].presence || DEFAULT_PAGE_SIZE
|
98
|
+
end
|
99
|
+
|
100
|
+
def display_pagination?
|
101
|
+
return false if group.present?
|
102
|
+
return false unless paginate && paginated_collection.respond_to?(:total_count)
|
103
|
+
|
104
|
+
paginated_collection.total_count > (params[:page_size].presence&.to_i || DEFAULT_PAGE_SIZE)
|
105
|
+
end
|
106
|
+
|
107
|
+
def verify_paginate_and_group
|
108
|
+
return unless paginate == false && group.present?
|
109
|
+
|
110
|
+
raise ArgumentError, 'Cannot specify both `paginate: false` and a `group` argument.'
|
111
|
+
end
|
112
|
+
|
113
|
+
def row_class_mapper
|
114
|
+
row_class.is_a?(Proc) ? row_class : proc { row_class }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|