active_element 0.0.1 → 0.0.3
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 +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
|