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.
- checksums.yaml +4 -4
- data/.rubocop.yml +12 -0
- data/.strong_versions.yml +2 -0
- data/Gemfile +11 -2
- data/Gemfile.lock +230 -3
- 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,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,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,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>
|