active_element 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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 +11 -2
  5. data/Gemfile.lock +230 -3
  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: "/active_element/#{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>