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.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +12 -0
  3. data/.strong_versions.yml +2 -0
  4. data/Gemfile +10 -2
  5. data/Gemfile.lock +229 -4
  6. data/Rakefile +1 -0
  7. data/active_element.gemspec +7 -0
  8. data/app/assets/config/active_element/manifest.js +2 -0
  9. data/app/assets/javascripts/active_element/application.js +10 -0
  10. data/app/assets/javascripts/active_element/confirm.js +67 -0
  11. data/app/assets/javascripts/active_element/form.js +61 -0
  12. data/app/assets/javascripts/active_element/json_field.js +316 -0
  13. data/app/assets/javascripts/active_element/pagination.js +18 -0
  14. data/app/assets/javascripts/active_element/search_field.js +127 -0
  15. data/app/assets/javascripts/active_element/secret.js +40 -0
  16. data/app/assets/javascripts/active_element/setup.js +36 -0
  17. data/app/assets/javascripts/active_element/theme.js +42 -0
  18. data/app/assets/stylesheets/active_element/_variables.scss +142 -0
  19. data/app/assets/stylesheets/active_element/application.scss +77 -0
  20. data/app/controllers/active_element/application_controller.rb +41 -0
  21. data/app/controllers/active_element/text_searches_controller.rb +189 -0
  22. data/app/views/active_element/components/_horizontal_tabs.html.erb +32 -0
  23. data/app/views/active_element/components/_vertical_tabs.html.erb +38 -0
  24. data/app/views/active_element/components/button.html.erb +27 -0
  25. data/app/views/active_element/components/fields/_boolean.html.erb +11 -0
  26. data/app/views/active_element/components/form/_check_box.html.erb +3 -0
  27. data/app/views/active_element/components/form/_check_boxes.html.erb +33 -0
  28. data/app/views/active_element/components/form/_field.html.erb +28 -0
  29. data/app/views/active_element/components/form/_generic_field.html.erb +3 -0
  30. data/app/views/active_element/components/form/_json.html.erb +12 -0
  31. data/app/views/active_element/components/form/_label.html.erb +17 -0
  32. data/app/views/active_element/components/form/_option_groups_summary.html.erb +17 -0
  33. data/app/views/active_element/components/form/_select.html.erb +4 -0
  34. data/app/views/active_element/components/form/_summary.html.erb +40 -0
  35. data/app/views/active_element/components/form/_templates.html.erb +85 -0
  36. data/app/views/active_element/components/form/_text_area.html.erb +4 -0
  37. data/app/views/active_element/components/form/_text_search.html.erb +16 -0
  38. data/app/views/active_element/components/form.html.erb +78 -0
  39. data/app/views/active_element/components/json.html.erb +8 -0
  40. data/app/views/active_element/components/page_description.html.erb +3 -0
  41. data/app/views/active_element/components/secret/_field.html.erb +1 -0
  42. data/app/views/active_element/components/secret/_templates.html.erb +11 -0
  43. data/app/views/active_element/components/table/_collection_row.html.erb +30 -0
  44. data/app/views/active_element/components/table/_grouped_collection.html.erb +88 -0
  45. data/app/views/active_element/components/table/_pagination.html.erb +17 -0
  46. data/app/views/active_element/components/table/_ungrouped_collection.html.erb +49 -0
  47. data/app/views/active_element/components/table/collection.html.erb +39 -0
  48. data/app/views/active_element/components/table/item.html.erb +39 -0
  49. data/app/views/active_element/components/tabs.html.erb +7 -0
  50. data/app/views/active_element/decorators/_boolean.html.erb +5 -0
  51. data/app/views/active_element/decorators/_date.html.erb +3 -0
  52. data/app/views/active_element/decorators/_datetime.html.erb +3 -0
  53. data/app/views/active_element/decorators/_time.html.erb +3 -0
  54. data/app/views/active_element/forbidden.html.erb +33 -0
  55. data/app/views/active_element/main_menu/_item.html.erb +9 -0
  56. data/app/views/active_element/navbar/_menu.html.erb +30 -0
  57. data/app/views/active_element/theme/_select.html.erb +1 -0
  58. data/app/views/active_element/theme/_templates.html.erb +6 -0
  59. data/app/views/kaminari/_first_page.html.erb +3 -0
  60. data/app/views/kaminari/_gap.html.erb +3 -0
  61. data/app/views/kaminari/_last_page.html.erb +3 -0
  62. data/app/views/kaminari/_next_page.html.erb +3 -0
  63. data/app/views/kaminari/_page.html.erb +9 -0
  64. data/app/views/kaminari/_paginator.html.erb +17 -0
  65. data/app/views/kaminari/_prev_page.html.erb +3 -0
  66. data/app/views/layouts/active_element.html.erb +65 -0
  67. data/app/views/layouts/active_element_error.html.erb +40 -0
  68. data/config/routes.rb +5 -0
  69. data/lib/active_element/active_menu_link.rb +80 -0
  70. data/lib/active_element/active_record_text_search_authorization.rb +12 -0
  71. data/lib/active_element/colorized_string.rb +33 -0
  72. data/lib/active_element/component.rb +122 -0
  73. data/lib/active_element/components/button.rb +156 -0
  74. data/lib/active_element/components/collection_table.rb +118 -0
  75. data/lib/active_element/components/form.rb +210 -0
  76. data/lib/active_element/components/item_table.rb +57 -0
  77. data/lib/active_element/components/json.rb +31 -0
  78. data/lib/active_element/components/link_helpers.rb +9 -0
  79. data/lib/active_element/components/page_description.rb +28 -0
  80. data/lib/active_element/components/secret_fields.rb +15 -0
  81. data/lib/active_element/components/tab.rb +37 -0
  82. data/lib/active_element/components/tabs.rb +35 -0
  83. data/lib/active_element/components/translations.rb +18 -0
  84. data/lib/active_element/components/util/association_mapping.rb +80 -0
  85. data/lib/active_element/components/util/decorator.rb +107 -0
  86. data/lib/active_element/components/util/display_value_mapping.rb +48 -0
  87. data/lib/active_element/components/util/field_mapping.rb +144 -0
  88. data/lib/active_element/components/util/form_field_mapping.rb +104 -0
  89. data/lib/active_element/components/util/form_value_mapping.rb +49 -0
  90. data/lib/active_element/components/util/i18n.rb +66 -0
  91. data/lib/active_element/components/util/record_mapping.rb +111 -0
  92. data/lib/active_element/components/util.rb +43 -0
  93. data/lib/active_element/components.rb +20 -0
  94. data/lib/active_element/controller_action.rb +91 -0
  95. data/lib/active_element/engine.rb +26 -0
  96. data/lib/active_element/permissions_check.rb +101 -0
  97. data/lib/active_element/rails_component.rb +40 -0
  98. data/lib/active_element/route.rb +112 -0
  99. data/lib/active_element/routes.rb +62 -0
  100. data/lib/active_element/version.rb +1 -1
  101. data/lib/active_element.rb +91 -1
  102. data/lib/tasks/active_element.rake +23 -0
  103. data/rspec-documentation/dummy +1 -0
  104. data/rspec-documentation/pages/Components/Forms.md +1 -0
  105. data/rspec-documentation/pages/Components/Tables.md +47 -0
  106. data/rspec-documentation/pages/Components/Tabs.md +1 -0
  107. data/rspec-documentation/pages/Components.md +1 -0
  108. data/rspec-documentation/pages/Decorators/Inline Decorators.md +1 -0
  109. data/rspec-documentation/pages/Decorators/View Decorators.md +1 -0
  110. data/rspec-documentation/pages/Index.md +3 -0
  111. data/rspec-documentation/pages/Util/I18n.md +1 -0
  112. data/rspec-documentation/spec_helper.rb +35 -0
  113. metadata +191 -3
@@ -0,0 +1,142 @@
1
+ // Sandstone 5.2.2
2
+ // Bootswatch
3
+
4
+ $theme: "sandstone" !default;
5
+
6
+ //
7
+ // Color system
8
+ //
9
+
10
+ $white: #fff !default;
11
+ $gray-100: #f8f9fa !default;
12
+ $gray-200: #f8f5f0 !default;
13
+ $gray-300: #dfd7ca !default;
14
+ $gray-400: #ced4da !default;
15
+ $gray-500: #98978b !default;
16
+ $gray-600: #8e8c84 !default;
17
+ $gray-700: #495057 !default;
18
+ $gray-800: #3e3f3a !default;
19
+ $gray-900: #212529 !default;
20
+ $black: #000 !default;
21
+
22
+ $blue: #325d88 !default;
23
+ $indigo: #6610f2 !default;
24
+ $purple: #6f42c1 !default;
25
+ $pink: #e83e8c !default;
26
+ $red: #d9534f !default;
27
+ $orange: #f47c3c !default;
28
+ $yellow: #ffc107 !default;
29
+ $green: #93c54b !default;
30
+ $teal: #20c997 !default;
31
+ $cyan: #29abe0 !default;
32
+
33
+ $primary: $blue !default;
34
+ $secondary: $gray-600 !default;
35
+ $success: $green !default;
36
+ $info: $cyan !default;
37
+ $warning: $orange !default;
38
+ $danger: $red !default;
39
+ $light: $gray-200 !default;
40
+ $dark: $gray-800 !default;
41
+
42
+ $theme-select-icon-dark: $blue;
43
+ $theme-select-icon-light: $orange;
44
+
45
+ $min-contrast-ratio: 2 !default;
46
+
47
+ // Body
48
+
49
+ $body-color: $gray-800 !default;
50
+
51
+ // Links
52
+
53
+ $link-color: $success !default;
54
+
55
+ // Fonts
56
+
57
+ // stylelint-disable-next-line value-keyword-case
58
+ $font-family-sans-serif: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !default;
59
+ $headings-font-weight: 400 !default;
60
+
61
+ // Dropdowns
62
+
63
+ $dropdown-link-color: $gray-600 !default;
64
+ $dropdown-link-hover-color: $dropdown-link-color !default;
65
+ $dropdown-link-hover-bg: $gray-200 !default;
66
+ $dropdown-link-active-color: $dropdown-link-color !default;
67
+ $dropdown-link-active-bg: $dropdown-link-hover-bg !default;
68
+
69
+ // Navs
70
+
71
+ $nav-link-padding-x: .9rem !default;
72
+ $nav-link-disabled-color: $gray-300 !default;
73
+ $nav-tabs-border-color: $gray-300 !default;
74
+ $nav-tabs-link-hover-border-color: $gray-300 !default;
75
+ $nav-tabs-link-active-bg: $white !default;
76
+ $nav-pills-link-active-color: $gray-600 !default;
77
+ $nav-pills-link-active-bg: $gray-200 !default;
78
+
79
+ // Navbar
80
+
81
+ $navbar-dark-hover-color: $white !default;
82
+ $navbar-light-hover-color: $black !default;
83
+ $navbar-light-active-color: $black !default;
84
+
85
+ // Pagination
86
+
87
+ $pagination-color: $gray-600 !default;
88
+ $pagination-bg: $gray-200 !default;
89
+ $pagination-border-color: $gray-300 !default;
90
+ $pagination-hover-color: $pagination-color !default;
91
+ $pagination-active-color: $pagination-color !default;
92
+ $pagination-active-bg: $gray-300 !default;
93
+ $pagination-active-border-color: $gray-300 !default;
94
+ $pagination-disabled-color: $gray-300 !default;
95
+ $pagination-disabled-bg: $gray-200 !default;
96
+ $pagination-disabled-border-color: $pagination-border-color !default;
97
+
98
+ // Cards
99
+
100
+ $card-border-color: rgba($gray-300, .75) !default;
101
+ $card-cap-bg: rgba($gray-200, .25) !default;
102
+
103
+ // Popovers
104
+
105
+ $popover-header-bg: $gray-200 !default;
106
+
107
+ // Modals
108
+
109
+ $modal-content-border-color: $gray-300 !default;
110
+ $modal-header-border-color: $modal-content-border-color !default;
111
+
112
+ // Progress bars
113
+
114
+ $progress-bg: $gray-300 !default;
115
+ $progress-border-radius: 10px !default;
116
+ $progress-bar-color: $primary !default;
117
+
118
+ // List group
119
+
120
+ $list-group-border-color: $gray-300 !default;
121
+ $list-group-hover-bg: $gray-200 !default;
122
+ $list-group-active-color: $body-color !default;
123
+ $list-group-active-bg: $gray-200 !default;
124
+ $list-group-active-border-color: $gray-300 !default;
125
+ $list-group-disabled-color: $gray-500 !default;
126
+ $list-group-disabled-bg: $white !default;
127
+ $list-group-action-color: $list-group-active-color !default;
128
+ $list-group-action-active-color: $list-group-active-color !default;
129
+ $list-group-action-active-bg: $gray-300 !default;
130
+
131
+ // Breadcrumbs
132
+
133
+ $breadcrumb-padding-y: .375rem !default;
134
+ $breadcrumb-padding-x: .75rem !default;
135
+ $breadcrumb-bg: $pagination-bg !default;
136
+ $breadcrumb-border-radius: .25rem !default;
137
+
138
+ // Close
139
+
140
+ $btn-close-color: $white !default;
141
+ $btn-close-opacity: .8 !default;
142
+ $btn-close-hover-opacity: 1 !default;
@@ -0,0 +1,77 @@
1
+ @import "variables";
2
+ @import "bootstrap";
3
+
4
+ .navbar-brand {
5
+ }
6
+
7
+ td.action-column {
8
+ width: 2rem;
9
+ }
10
+
11
+ .action-button {
12
+ margin-right: 0.5rem;
13
+ min-width: 5rem;
14
+ }
15
+
16
+ table {
17
+ td.action-column {
18
+ padding-right: 0.2rem;
19
+ padding-left: 0.2rem;
20
+ }
21
+
22
+ .action-button {
23
+ margin-right: 0.2rem;
24
+ min-width: 2rem;
25
+
26
+ .button-title {
27
+ display: none;
28
+ }
29
+ }
30
+ }
31
+
32
+ form {
33
+ label {
34
+ display: inline;
35
+ }
36
+ }
37
+
38
+ .json-field {
39
+ .form-control, .form-select {
40
+ width: calc(100% - 2.5rem);
41
+ display: inline;
42
+ }
43
+
44
+ .action-button {
45
+ min-width: 2rem;
46
+ margin-left: 0.2rem;
47
+
48
+ .button-title {
49
+ display: none;
50
+ }
51
+ }
52
+ }
53
+
54
+ #theme-select {
55
+ font-size: 1.5rem;
56
+ padding: 0.2rem 0.8rem;
57
+ position: absolute;
58
+ right: 0;
59
+
60
+ a {
61
+ &.dark-theme {
62
+ color: #{$theme-select-icon-dark};
63
+
64
+ &:hover, &:active {
65
+ color: #{lighten($theme-select-icon-dark, 10%)};
66
+ }
67
+ }
68
+
69
+ &.light-theme {
70
+ color: #{$theme-select-icon-light};
71
+
72
+ &:hover, &:active {
73
+ color: #{lighten($theme-select-icon-light, 10%)};
74
+ }
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ # Base controller for all ActiveElement API admin front ends, provides standard layout, menu,
5
+ # authentication as Superuser, and standardised HTML widgets.
6
+ class ApplicationController < ActionController::Base
7
+ include ActionView::Helpers::TagHelper
8
+
9
+ layout 'active_element'
10
+
11
+ before_action -> { authenticate_user! }
12
+ before_action -> { ActiveElement::ControllerAction.new(self).process_action }
13
+
14
+ helper_method :active_element_component
15
+ helper_method :render_active_element_hook
16
+
17
+ def self.permit_user(permissions, **kwargs)
18
+ active_element_permissions << [permissions, kwargs]
19
+
20
+ nil
21
+ end
22
+
23
+ def active_element_component
24
+ @active_element_component ||= ActiveElement::Component.new(self)
25
+ end
26
+
27
+ def render_active_element_hook(hook)
28
+ render_to_string partial: hook
29
+ rescue ActionView::MissingTemplate
30
+ nil
31
+ end
32
+
33
+ def missing_template_store
34
+ @missing_template_store ||= {}
35
+ end
36
+
37
+ def self.active_element_permissions
38
+ @active_element_permissions ||= []
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ # Used by auto-complete search field for executing a text search on the provided model and
5
+ # attributes.
6
+ #
7
+ # The user must have the permission `can_create_<application_name>_active_element_text_searches`.
8
+ #
9
+ # A model must call `authorize_active_element_text_search_for` to enable text search. e.g.:
10
+ #
11
+ # class MyModel < ApplicationRecord
12
+ # authorize_active_element_text_search_for :name, exposes: [:email]
13
+ # end
14
+ #
15
+ # This will allow searching on the `name` column and permits returning each matching record's
16
+ # `:email` column's value.
17
+ #
18
+ # TODO: Refactor logic into separate classes.
19
+ # rubocop:disable Metrics/ClassLength
20
+ class TextSearchesController < ApplicationController
21
+ DEFAULT_LIMIT = 50
22
+
23
+ before_action :verify_parameters
24
+ before_action :verify_model
25
+
26
+ def create
27
+ render json: { results: results, request_id: params[:request_id] }
28
+ end
29
+
30
+ private
31
+
32
+ def verify_parameters
33
+ return if %i[model attributes value query].all? { |parameter| params[parameter].present? }
34
+
35
+ render json: { message: 'Must provide parameters: [model, attributes, value, query] for text search.' },
36
+ status: :unprocessable_entity
37
+ end
38
+
39
+ def verify_model
40
+ return if [model, search_columns, value_column].all?(&:present?) && authorized?
41
+
42
+ render json: { message: verify_fail_message }, status: :unprocessable_entity
43
+ end
44
+
45
+ def verify_fail_message
46
+ requested = %i[model attributes value].index_with { |key| params[key] }
47
+ .compact_blank
48
+ .map { |key, value| "#{key}: #{value}" }
49
+ "Unpermitted or unavailable search for: { #{requested.join(', ')} }"
50
+ end
51
+
52
+ def results
53
+ @results ||= model.where(*whereclause)
54
+ .limit(limit)
55
+ .pluck(value_column.name, *search_columns.map(&:name))
56
+ .map { |value, *attributes| result(value, attributes) }
57
+ end
58
+
59
+ def result(value, attributes)
60
+ { value: value, attributes: attributes.reject { |attribute| attribute == value } }
61
+ end
62
+
63
+ def model
64
+ @model ||= params[:model].camelize(:upper).safe_constantize
65
+ end
66
+
67
+ def query
68
+ params[:query]
69
+ end
70
+
71
+ def value_column
72
+ return nil if params[:value].blank?
73
+
74
+ @value_column ||= model&.columns&.find { |column| column.name == params[:value] }
75
+ end
76
+
77
+ def search_columns
78
+ return nil if params[:attributes].blank?
79
+
80
+ @search_columns ||= params[:attributes].map { |attribute| column_for(attribute) }.compact
81
+ end
82
+
83
+ def column_for(attribute)
84
+ matched_column = model&.columns&.find { |column| column.name == attribute }
85
+ return nil if matched_column.blank?
86
+
87
+ compatible_column?(matched_column) ? matched_column : nil
88
+ end
89
+
90
+ def authorized?
91
+ model.authorized_active_element_text_search_fields&.any? do |field, exposed|
92
+ exposed = [exposed] unless exposed.is_a?(Array)
93
+ authorized_field?(field, exposed)
94
+ end
95
+ end
96
+
97
+ def authorized_field?(field, exposed)
98
+ return false unless search_columns.map { |column| column.name.to_sym }.include?(field.to_sym)
99
+ return false unless exposed&.map(&:to_sym)&.include?(value_column.name.to_sym)
100
+
101
+ true
102
+ end
103
+
104
+ def whereclause
105
+ clauses = search_columns.map { |column| "#{column.name} #{operator(column)} ?" }
106
+ [clauses.join(' OR '), search_columns.map { |column| search_param(column) }].flatten
107
+ end
108
+
109
+ def operator(column)
110
+ case column.type
111
+ when :string
112
+ model.connection.adapter_name == 'SQLite' ? 'LIKE' : 'ILIKE'
113
+ else
114
+ '='
115
+ end
116
+ end
117
+
118
+ def compatible_column?(column) # rubocop:disable Metrics/MethodLength
119
+ case column.type
120
+ when :string
121
+ true
122
+ when :integer
123
+ integer?
124
+ when :float
125
+ float?
126
+ when :decimal
127
+ decimal?
128
+ else
129
+ Rails.logger.info("Skipping query `#{query}` for incompatible column: #{column.name}")
130
+ false
131
+ end
132
+ end
133
+
134
+ def integer?
135
+ Integer(query)
136
+ true
137
+ rescue ArgumentError
138
+ false
139
+ end
140
+
141
+ def float?
142
+ Float(query)
143
+ true
144
+ rescue ArgumentError
145
+ false
146
+ end
147
+
148
+ def decimal?
149
+ BigDecimal(query)
150
+ true
151
+ rescue ArgumentError
152
+ false
153
+ end
154
+
155
+ def search_param(column)
156
+ case column.type
157
+ when :string
158
+ "#{query}%"
159
+ else
160
+ query
161
+ end
162
+ end
163
+
164
+ def limit
165
+ DEFAULT_LIMIT
166
+ end
167
+
168
+ def permissions_check
169
+ @permissions_check ||= PermissionsCheck.new(
170
+ required: [],
171
+ actual: current_user.permissions,
172
+ controller_path: controller_path,
173
+ action_name: action_name,
174
+ rails_component: rails_component
175
+ )
176
+ end
177
+
178
+ def required_permissions
179
+ application_name = rails_component.application_name
180
+ permission = "can_text_search_#{application_name}_#{params[:model]&.pluralize}"
181
+ [[permission, { only: :create }]]
182
+ end
183
+
184
+ def rails_component
185
+ @rails_component ||= RailsComponent.new(Rails)
186
+ end
187
+ end
188
+ # rubocop:enable Metrics/ClassLength
189
+ end
@@ -0,0 +1,32 @@
1
+ <nav class="<%= class_name %>">
2
+ <div class="nav nav-tabs" id="nav-tab" role="tablist">
3
+ <% tabs.each do |tab| %>
4
+ <%=
5
+ link_to tab.title,
6
+ tab.path,
7
+ class: "nav-link #{tab.selected? ? 'active' : nil}",
8
+ id: "nav-#{tab.identifier}-tab",
9
+ type: 'button',
10
+ role: 'tab',
11
+ :'aria-controls' => "nav-#{tab.identifier}",
12
+ :'aria-selected' => tab.selected? ? 'true' : 'false'
13
+ %>
14
+ <% end %>
15
+ </div>
16
+ </nav>
17
+
18
+ <% tabs.each do |tab| %>
19
+ <div class="tab-content" id="nav-tabContent">
20
+ <div
21
+ class="p-3 tab-pane fade <%= tab.selected? ? 'show active' : nil %> <%= tab.identifier %>"
22
+ id="nav-credentials"
23
+ role="tabpanel"
24
+ aria-labelledby="nav-<%= tab.identifier %>-tab"
25
+ tabindex="0"
26
+ >
27
+ <div class="<%= tab.identifier %>">
28
+ <%= tab.content %>
29
+ </div>
30
+ </div>
31
+ </div>
32
+ <% end %>
@@ -0,0 +1,38 @@
1
+ <div class="row">
2
+ <div class="col-3">
3
+ <nav class="<%= class_name %>">
4
+ <div class="nav nav-pills flex-column" id="nav-tab" role="tablist">
5
+ <% tabs.sort_by { |tab| tab.selected? ? 0 : 1 }.each do |tab| %>
6
+ <%=
7
+ link_to tab.title,
8
+ tab.path,
9
+ class: "nav-link #{tab.selected? ? 'active' : nil}",
10
+ id: "nav-#{tab.identifier}-tab",
11
+ type: 'button',
12
+ role: 'tab',
13
+ :'aria-controls' => "nav-#{tab.identifier}",
14
+ :'aria-selected' => tab.selected? ? 'true' : 'false'
15
+ %>
16
+ <% end %>
17
+ </div>
18
+ </nav>
19
+ </div>
20
+
21
+ <div class="col-9">
22
+ <% tabs.each do |tab| %>
23
+ <div class="tab-content" id="nav-tabContent">
24
+ <div
25
+ class="ps-3 tab-pane fade <%= tab.selected? ? 'show active' : nil %> <%= tab.identifier %>"
26
+ id="nav-credentials"
27
+ role="tabpanel"
28
+ aria-labelledby="nav-<%= tab.identifier %>-tab"
29
+ tabindex="0"
30
+ >
31
+ <div class="<%= tab.identifier %>">
32
+ <%= tab.content %>
33
+ </div>
34
+ </div>
35
+ </div>
36
+ <% end %>
37
+ </div>
38
+ </div>
@@ -0,0 +1,27 @@
1
+
2
+ <%=
3
+ link_to(
4
+ path.presence || '#',
5
+ method: path.present? && method != :get ? method : nil,
6
+ data: { confirm_action: confirm },
7
+ class: "btn #{button_class} #{float_class} #{kwargs_class}",
8
+ title: title,
9
+ **kwargs
10
+ ) do %>
11
+ <% if block_given %>
12
+ <%= raw content %>
13
+ <% else %>
14
+ <span class="text-nowrap">
15
+ <% if icon.present? %>
16
+ <i class="fa-solid fa-<%= icon %>"></i>
17
+ <% elsif type == :show %>
18
+ <i class="fa-solid fa-eye"></i>
19
+ <% elsif type == :edit %>
20
+ <i class="fa-solid fa-pen"></i>
21
+ <% elsif type == :destroy %>
22
+ <i class="fa-solid fa-square-xmark"></i>
23
+ <% end %>
24
+ <span class="button-title"><%= title %></span>
25
+ </span>
26
+ <% end %>
27
+ <% end %>
@@ -0,0 +1,11 @@
1
+ <% if value %>
2
+ <span class="text-success">
3
+ <i class="fa-solid fa-fw fa-check"></i>
4
+ Yes
5
+ </span>
6
+ <% else %>
7
+ <span class="text-danger">
8
+ <i class="fa-solid fa-fw fa-xmark"></i>
9
+ No
10
+ </span>
11
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <%= form.check_box field, checked: ['1', true].include?(component.value_for(field)),
2
+ class: "form-check-input #{component.valid?(field) ? nil : 'is-invalid'}",
3
+ **options %>
@@ -0,0 +1,33 @@
1
+ <% if options.key?(:option_groups) %>
2
+ <% options[:option_groups].each do |group_title, group, group_options| %>
3
+ <div class="p-3">
4
+ <a name="<%= ActiveElement::Components::Util::I18n.class_name(group_title) %>"></a>
5
+ <%= component.page_section_title group_title %>
6
+ <%= form.fields_for field do |subform| %>
7
+ <% group.each do |label, name, checked| %>
8
+ <%= subform.check_box(name, checked: checked, class: 'me-2') %>
9
+ <%= subform.label name, label %>
10
+ <br/>
11
+ <% end %>
12
+ <% end %>
13
+
14
+ </div>
15
+ <hr/>
16
+ <% end %>
17
+ <% else %>
18
+ <div class="container w-100">
19
+ <%= form.fields_for field do |subform| %>
20
+ <% options.fetch(:options).each_slice(options.fetch(:columns, 1)) do |slice| %>
21
+ <div class="row w-100">
22
+ <% slice.each do |label, name, checked| %>
23
+ <div class="col">
24
+ <%= subform.check_box(name, checked: checked, class: 'me-2') %>
25
+ <%= subform.label name, label %>
26
+ </div>
27
+ <br/>
28
+ <% end %>
29
+ </div>
30
+ <% end %>
31
+ <% end %>
32
+ </div>
33
+ <% end %>
@@ -0,0 +1,28 @@
1
+ <% if type == :select %>
2
+ <%= render partial: 'active_element/components/form/select',
3
+ locals: { form: form, field: field, options: options, component: component } %>
4
+ <% elsif type == :check_boxes %>
5
+ <%= render partial: 'active_element/components/form/check_boxes',
6
+ locals: { field: field, form: form, options: options, component: component } %>
7
+ <% elsif type == :json_field %>
8
+ <%= render partial: 'active_element/components/form/json',
9
+ locals: { field: field, form: form, options: options, component: component } %>
10
+ <% elsif type == :text_search_field %>
11
+ <%= render partial: 'active_element/components/form/text_search',
12
+ locals: { form_id: id, field: field, form: form, options: options, component: component } %>
13
+ <% elsif type == :check_box %>
14
+ <%= render partial: 'active_element/components/form/check_box',
15
+ locals: { form: form, type: type, field: field, options: options, component: component } %>
16
+ <% elsif type == :text_area %>
17
+ <%= render partial: 'active_element/components/form/text_area',
18
+ locals: { form: form, type: type, field: field, options: options, component: component } %>
19
+ <% else %>
20
+ <%= render partial: 'active_element/components/form/generic_field',
21
+ locals: { form: form, type: type, field: field, options: options, component: component } %>
22
+ <% end %>
23
+
24
+ <% unless component.valid?(field) %>
25
+ <p class="text-danger pt-1 m-0 validation-error-message">
26
+ <%= record.errors.full_messages_for(field).join(', ') %>
27
+ </p>
28
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <%= form.public_send(type, field, value: component.value_for(field),
2
+ class: "form-control #{component.valid?(field) ? nil : 'is-invalid'}",
3
+ **options) %>
@@ -0,0 +1,12 @@
1
+ <div class="col-sm-10 json-field form-group"
2
+ data-data-key="<%= ActiveElement::Components::Util.json_name("#{form.object_name}.#{field}") %>"
3
+ >
4
+
5
+ </div>
6
+
7
+ <%=
8
+ component.json(
9
+ ActiveElement::Components::Util.json_name("#{form.object_name}.#{field}"),
10
+ { data: component.value_for(field, options), schema: component.schema_for(field, options) }
11
+ )
12
+ %>
@@ -0,0 +1,17 @@
1
+ <%= form.label field, options[:label] do %>
2
+ <% if options[:description].present? %>
3
+ <%= options[:label] %>
4
+ <button type="button"
5
+ style="background: none; border: none; outline: 0; position: absolute; margin-top: 0.3rem"
6
+ data-bs-toggle="popover"
7
+ title="<%= options.fetch(:label) %>"
8
+ data-bs-content="<%= options[:description] %>">
9
+ <i class="text-secondary fa-solid fa-circle-info"></i>
10
+ </button>
11
+ <% end %>
12
+ <% end %>
13
+
14
+ <% if type == :check_boxes && options[:option_groups].present? %>
15
+ <%= render partial: 'active_element/components/form/option_groups_summary',
16
+ locals: { option_groups: options[:option_groups] } %>
17
+ <% end %>
@@ -0,0 +1,17 @@
1
+ <ol class="list-group pt-2">
2
+ <% option_groups.each do |group_title, group, group_options| %>
3
+ <li class="list-group-item d-flex justify-content-between align-items-start">
4
+ <div class="ms-2 me-auto">
5
+ <a style="font-size: 0.8rem;" href="#<%= ActiveElement::Components::Util::I18n.class_name(group_title) %>">
6
+ <%= group_title %>
7
+ </a>
8
+ </div>
9
+ <% if group_options.key?(:count) %>
10
+ <span
11
+ class="badge bg-<%= group_options[:count].positive? ? 'primary' : 'secondary' %> rounded-pill">
12
+ <%= group_options[:count] %>
13
+ </span>
14
+ <% end %>
15
+ </li>
16
+ <% end %>
17
+ </ol>
@@ -0,0 +1,4 @@
1
+ <%= form.select field,
2
+ component.options_for_select(field, options),
3
+ { selected: component.value_for(field) },
4
+ { class: "form-select #{component.valid?(field) ? nil : 'is-invalid'}" } %>