active_element 0.0.10 → 0.0.11
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 -2
- data/.strong_versions.yml +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +108 -75
- data/Makefile +10 -0
- data/active_element.gemspec +1 -1
- data/app/assets/javascripts/active_element/application.js +1 -0
- data/app/assets/javascripts/active_element/form.js +16 -32
- data/app/assets/javascripts/active_element/json_field.js +391 -135
- data/app/assets/javascripts/active_element/setup.js +13 -8
- data/app/assets/javascripts/active_element/text_search_field.js +27 -28
- data/app/assets/javascripts/active_element/theme.js +1 -1
- data/app/assets/javascripts/active_element/timezones.js +6 -0
- data/app/assets/stylesheets/active_element/_dark.scss +86 -0
- data/app/assets/stylesheets/active_element/_variables.scss +2 -1
- data/app/assets/stylesheets/active_element/application.scss +166 -33
- data/app/controllers/active_element/application_controller.rb +5 -0
- data/app/controllers/concerns/active_element/default_controller_actions.rb +38 -0
- data/app/views/active_element/_user.html.erb +20 -0
- data/app/views/active_element/components/fields/_json.html.erb +24 -0
- data/app/views/active_element/components/form/_check_box.html.erb +1 -0
- data/app/views/active_element/components/form/_check_boxes.html.erb +1 -1
- data/app/views/active_element/components/form/_datetime_range_field.html.erb +14 -0
- data/app/views/active_element/components/form/_field.html.erb +10 -7
- data/app/views/active_element/components/form/_generic_field.html.erb +1 -0
- data/app/views/active_element/components/form/_json.html.erb +10 -2
- data/app/views/active_element/components/form/_label.html.erb +12 -1
- data/app/views/active_element/components/form/_select.html.erb +4 -1
- data/app/views/active_element/components/form/_summary.html.erb +11 -1
- data/app/views/active_element/components/form/_templates.html.erb +37 -22
- data/app/views/active_element/components/form/_text_area.html.erb +2 -1
- data/app/views/active_element/components/form/_text_search.html.erb +7 -3
- data/app/views/active_element/components/form.html.erb +20 -17
- data/app/views/active_element/components/json.html.erb +1 -0
- data/app/views/active_element/components/navbar.html.erb +26 -0
- data/app/views/active_element/components/table/_collection_row.html.erb +2 -1
- data/app/views/active_element/components/table/_field.html.erb +8 -0
- data/app/views/active_element/components/table/collection.html.erb +1 -1
- data/app/views/active_element/components/table/item.html.erb +5 -4
- data/app/views/active_element/default_views/edit.html.erb +5 -0
- data/app/views/active_element/default_views/index.html.erb +15 -0
- data/app/views/active_element/default_views/new.html.erb +4 -0
- data/app/views/active_element/default_views/show.html.erb +7 -0
- data/app/views/active_element/navbar/_menu.html.erb +1 -30
- data/app/views/active_element/theme/_select.html.erb +1 -1
- data/app/views/layouts/active_element.html.erb +16 -1
- data/config/brakeman.ignore +48 -0
- data/example_app/.gitattributes +7 -0
- data/example_app/.gitignore +35 -0
- data/example_app/.ruby-version +1 -0
- data/example_app/Gemfile +34 -0
- data/example_app/Gemfile.lock +296 -0
- data/example_app/README.md +24 -0
- data/example_app/Rakefile +6 -0
- data/example_app/app/assets/config/manifest.js +4 -0
- data/example_app/app/assets/images/.keep +0 -0
- data/example_app/app/assets/stylesheets/application.css +15 -0
- data/example_app/app/channels/application_cable/channel.rb +4 -0
- data/example_app/app/channels/application_cable/connection.rb +4 -0
- data/example_app/app/controllers/application_controller.rb +12 -0
- data/example_app/app/controllers/concerns/.keep +0 -0
- data/example_app/app/controllers/pets_controller.rb +6 -0
- data/example_app/app/controllers/users_controller.rb +6 -0
- data/example_app/app/helpers/application_helper.rb +2 -0
- data/example_app/app/javascript/application.js +3 -0
- data/example_app/app/javascript/controllers/application.js +9 -0
- data/example_app/app/javascript/controllers/hello_controller.js +7 -0
- data/example_app/app/javascript/controllers/index.js +11 -0
- data/example_app/app/jobs/application_job.rb +7 -0
- data/example_app/app/mailers/application_mailer.rb +4 -0
- data/example_app/app/models/application_record.rb +3 -0
- data/example_app/app/models/concerns/.keep +0 -0
- data/example_app/app/models/pet.rb +3 -0
- data/example_app/app/models/user.rb +8 -0
- data/example_app/app/views/layouts/application.html.erb +16 -0
- data/example_app/app/views/layouts/mailer.html.erb +13 -0
- data/example_app/app/views/layouts/mailer.text.erb +1 -0
- data/example_app/app/views/pets/index.html.erb +3 -0
- data/example_app/app/views/users/show.html.erb +3 -0
- data/example_app/bin/bundle +109 -0
- data/example_app/bin/importmap +4 -0
- data/example_app/bin/rails +4 -0
- data/example_app/bin/rake +4 -0
- data/example_app/bin/setup +33 -0
- data/example_app/config/application.rb +22 -0
- data/example_app/config/boot.rb +4 -0
- data/example_app/config/cable.yml +10 -0
- data/example_app/config/credentials.yml.enc +1 -0
- data/example_app/config/database.yml +25 -0
- data/example_app/config/environment.rb +5 -0
- data/example_app/config/environments/development.rb +70 -0
- data/example_app/config/environments/production.rb +93 -0
- data/example_app/config/environments/test.rb +60 -0
- data/example_app/config/importmap.rb +7 -0
- data/example_app/config/initializers/assets.rb +12 -0
- data/example_app/config/initializers/content_security_policy.rb +25 -0
- data/example_app/config/initializers/devise.rb +16 -0
- data/example_app/config/initializers/filter_parameter_logging.rb +8 -0
- data/example_app/config/initializers/inflections.rb +16 -0
- data/example_app/config/initializers/permissions_policy.rb +11 -0
- data/example_app/config/locales/devise.en.yml +65 -0
- data/example_app/config/locales/en.yml +33 -0
- data/example_app/config/puma.rb +43 -0
- data/example_app/config/routes.rb +8 -0
- data/example_app/config/storage.yml +34 -0
- data/example_app/config.ru +6 -0
- data/example_app/db/migrate/20230616210539_create_pet.rb +12 -0
- data/example_app/db/migrate/20230616211328_devise_create_users.rb +46 -0
- data/example_app/db/schema.rb +37 -0
- data/example_app/db/seeds.rb +33 -0
- data/example_app/lib/assets/.keep +0 -0
- data/example_app/lib/tasks/.keep +0 -0
- data/example_app/log/.keep +0 -0
- data/example_app/public/404.html +67 -0
- data/example_app/public/422.html +67 -0
- data/example_app/public/500.html +66 -0
- data/example_app/public/apple-touch-icon-precomposed.png +0 -0
- data/example_app/public/apple-touch-icon.png +0 -0
- data/example_app/public/favicon.ico +0 -0
- data/example_app/public/robots.txt +1 -0
- data/example_app/storage/.keep +0 -0
- data/example_app/test/application_system_test_case.rb +5 -0
- data/example_app/test/channels/application_cable/connection_test.rb +11 -0
- data/example_app/test/controllers/.keep +0 -0
- data/example_app/test/fixtures/files/.keep +0 -0
- data/example_app/test/fixtures/users.yml +11 -0
- data/example_app/test/helpers/.keep +0 -0
- data/example_app/test/integration/.keep +0 -0
- data/example_app/test/mailers/.keep +0 -0
- data/example_app/test/models/.keep +0 -0
- data/example_app/test/models/user_test.rb +7 -0
- data/example_app/test/system/.keep +0 -0
- data/example_app/test/test_helper.rb +13 -0
- data/example_app/tmp/.keep +0 -0
- data/example_app/tmp/pids/.keep +0 -0
- data/example_app/tmp/storage/.keep +0 -0
- data/example_app/vendor/.keep +0 -0
- data/example_app/vendor/javascript/.keep +0 -0
- data/lib/active_element/component.rb +9 -2
- data/lib/active_element/components/collection_table.rb +9 -2
- data/lib/active_element/components/email_fields.rb +14 -0
- data/lib/active_element/components/form.rb +48 -17
- data/lib/active_element/components/navbar.rb +64 -0
- data/lib/active_element/components/phone_fields.rb +14 -0
- data/lib/active_element/components/text_search/authorization.rb +9 -6
- data/lib/active_element/components/text_search/component.rb +4 -2
- data/lib/active_element/components/text_search.rb +4 -0
- data/lib/active_element/components/util/association_mapping.rb +74 -19
- data/lib/active_element/components/util/display_value_mapping.rb +13 -4
- data/lib/active_element/components/util/form_field_mapping.rb +127 -10
- data/lib/active_element/components/util/form_value_mapping.rb +3 -3
- data/lib/active_element/components/util/i18n.rb +1 -1
- data/lib/active_element/components/util/record_mapping.rb +43 -11
- data/lib/active_element/components/util/record_path.rb +21 -4
- data/lib/active_element/components/util.rb +12 -5
- data/lib/active_element/components.rb +3 -0
- data/lib/active_element/controller_action.rb +8 -2
- data/lib/active_element/controller_interface.rb +47 -5
- data/lib/active_element/default_controller.rb +93 -0
- data/lib/active_element/default_record_params.rb +62 -0
- data/lib/active_element/default_text_search.rb +110 -0
- data/lib/active_element/json_field_schema.rb +59 -0
- data/lib/active_element/pre_render_processors/json.rb +98 -0
- data/lib/active_element/pre_render_processors.rb +11 -0
- data/lib/active_element/route.rb +12 -0
- data/lib/active_element/routes.rb +2 -1
- data/lib/active_element/version.rb +1 -1
- data/lib/active_element.rb +14 -32
- data/lib/tasks/active_element.rake +12 -1
- data/rspec-documentation/_head.html.erb +34 -0
- data/rspec-documentation/pages/000-Introduction.md +18 -0
- data/rspec-documentation/pages/005-Setup.md +75 -0
- data/rspec-documentation/pages/010-Components/Form Fields/Check Boxes.md +1 -0
- data/rspec-documentation/pages/010-Components/Form Fields/JSON/Controller Params.md +97 -0
- data/rspec-documentation/pages/010-Components/Form Fields/JSON/Schema.md +283 -0
- data/rspec-documentation/pages/010-Components/Form Fields/JSON/Types.md +36 -0
- data/rspec-documentation/pages/010-Components/Form Fields/JSON.md +70 -0
- data/rspec-documentation/pages/010-Components/Form Fields/Text Search.md +133 -0
- data/rspec-documentation/pages/010-Components/Form Fields.md +46 -0
- data/rspec-documentation/pages/010-Components/Forms.md +44 -0
- data/rspec-documentation/pages/010-Components/JSON Data.md +23 -0
- data/rspec-documentation/pages/010-Components/Navbar.md +56 -0
- data/rspec-documentation/pages/010-Components/Page Section Title.md +13 -0
- data/rspec-documentation/pages/010-Components/Page Subtitle.md +11 -0
- data/rspec-documentation/pages/010-Components/Page Title.md +11 -0
- data/rspec-documentation/pages/010-Components/Tables/Collection Table.md +29 -0
- data/rspec-documentation/pages/010-Components/Tables/Item Table.md +18 -0
- data/rspec-documentation/pages/010-Components/Tables/Options.md +19 -0
- data/rspec-documentation/pages/010-Components/Tables.md +29 -0
- data/rspec-documentation/pages/010-Components.md +15 -0
- data/rspec-documentation/pages/020-Access Control/010-Authentication.md +20 -0
- data/rspec-documentation/pages/020-Access Control/020-Authorization/Environments.md +9 -0
- data/rspec-documentation/pages/020-Access Control/020-Authorization/Permissions/Custom Routes.md +41 -0
- data/rspec-documentation/pages/020-Access Control/020-Authorization/Permissions.md +58 -0
- data/rspec-documentation/pages/020-Access Control/020-Authorization/Setup.md +27 -0
- data/rspec-documentation/pages/020-Access Control/020-Authorization.md +11 -0
- data/rspec-documentation/pages/020-Access Control.md +31 -0
- data/rspec-documentation/pages/040-Decorators/Inline Decorators.md +24 -0
- data/rspec-documentation/pages/040-Decorators/View Decorators.md +55 -0
- data/rspec-documentation/pages/040-Decorators.md +12 -0
- data/rspec-documentation/pages/300-Alternatives.md +21 -0
- data/rspec-documentation/pages/900-License.md +11 -0
- data/rspec-documentation/spec_helper.rb +53 -16
- data/rspec-documentation/support.rb +84 -0
- metadata +155 -14
- data/rspec-documentation/pages/Components/Forms.md +0 -1
- data/rspec-documentation/pages/Components/Tables.md +0 -47
- data/rspec-documentation/pages/Components.md +0 -1
- data/rspec-documentation/pages/Decorators/Inline Decorators.md +0 -1
- data/rspec-documentation/pages/Decorators/View Decorators.md +0 -1
- data/rspec-documentation/pages/Index.md +0 -3
- data/rspec-documentation/pages/Util/I18n.md +0 -1
- /data/rspec-documentation/pages/{Components → 010-Components}/Tabs.md +0 -0
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveElement
|
4
|
+
module Components
|
5
|
+
# A navigation bar providing links to areas within the application.
|
6
|
+
class Navbar
|
7
|
+
include Translations
|
8
|
+
|
9
|
+
attr_reader :controller
|
10
|
+
|
11
|
+
def initialize(controller, items: nil, fixed: true)
|
12
|
+
@controller = controller
|
13
|
+
@items = items
|
14
|
+
@fixed = fixed
|
15
|
+
end
|
16
|
+
|
17
|
+
def template
|
18
|
+
'active_element/components/navbar'
|
19
|
+
end
|
20
|
+
|
21
|
+
def locals
|
22
|
+
{
|
23
|
+
component: self,
|
24
|
+
items: items,
|
25
|
+
fixed: fixed
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def active_path_class(current_navbar_item:)
|
30
|
+
if ActiveMenuLink.new(
|
31
|
+
rails_component: RailsComponent.new(Rails),
|
32
|
+
navbar_items: items,
|
33
|
+
current_path: controller.request.path,
|
34
|
+
current_navbar_item: current_navbar_item,
|
35
|
+
controller_path: controller.controller_path,
|
36
|
+
action_name: controller.action_name
|
37
|
+
).active?
|
38
|
+
'active'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
attr_reader :fixed
|
45
|
+
|
46
|
+
def items
|
47
|
+
return @items unless @items.nil?
|
48
|
+
|
49
|
+
ActiveElement.eager_load_controllers
|
50
|
+
|
51
|
+
@items ||= user_routes(controller.active_element.current_user).available.select(&:primary?).map do |route|
|
52
|
+
{ path: route.path, title: route.title, spec: route.spec }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def user_routes(user)
|
57
|
+
ActiveElement::Routes.new(
|
58
|
+
permissions: user&.permissions,
|
59
|
+
rails_component: ActiveElement::RailsComponent.new(Rails)
|
60
|
+
)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveElement
|
4
|
+
module Components
|
5
|
+
# Provides a convenience method for detecting a field should be classified as a phone number.
|
6
|
+
module PhoneFields
|
7
|
+
PHONE_FIELDS = %w[phone telephone tel mobile].freeze
|
8
|
+
|
9
|
+
def phone_field?(field)
|
10
|
+
PHONE_FIELDS.any? { |phone_field| field.to_s.downcase.split('_').include?(phone_field) }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -20,12 +20,12 @@ module ActiveElement
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
-
def initialize(model:, params:, user:, search_columns:,
|
23
|
+
def initialize(model:, params:, user:, search_columns:, value_column:)
|
24
24
|
@model = model
|
25
25
|
@params = params
|
26
26
|
@user = user
|
27
27
|
@search_columns = search_columns
|
28
|
-
@
|
28
|
+
@value_column = value_column
|
29
29
|
end
|
30
30
|
|
31
31
|
def authorized?
|
@@ -45,14 +45,15 @@ module ActiveElement
|
|
45
45
|
|
46
46
|
private
|
47
47
|
|
48
|
-
attr_reader :model, :params, :user, :search_columns, :
|
48
|
+
attr_reader :model, :params, :user, :search_columns, :value_column
|
49
49
|
|
50
50
|
def development_message
|
51
51
|
paintbrush { green "Bypassed text search authorization in development environment: #{yellow message}" }
|
52
52
|
end
|
53
53
|
|
54
54
|
def missing_permissions
|
55
|
-
(search_columns +
|
55
|
+
(search_columns + [value_column]).reject { |column| user_permitted?(column) }
|
56
|
+
.compact_blank
|
56
57
|
.map { |column| permission_for(column.name) }
|
57
58
|
.uniq
|
58
59
|
.sort
|
@@ -87,6 +88,8 @@ module ActiveElement
|
|
87
88
|
end
|
88
89
|
|
89
90
|
def user_permitted?(column)
|
91
|
+
return false if column.blank?
|
92
|
+
|
90
93
|
user&.permissions&.include?(permission_for(column.name))
|
91
94
|
end
|
92
95
|
|
@@ -99,10 +102,10 @@ module ActiveElement
|
|
99
102
|
end
|
100
103
|
|
101
104
|
def authorized_model?
|
102
|
-
TextSearch.authorized_text_searches.any? do |authorized_model, search_fields,
|
105
|
+
TextSearch.authorized_text_searches.any? do |authorized_model, search_fields, value_field|
|
103
106
|
next false unless authorized_model == model
|
104
107
|
next false unless authorized_fields?(search_columns, Array(search_fields))
|
105
|
-
next false unless authorized_fields?(
|
108
|
+
next false unless authorized_fields?(Array(value_column), Array(value_field))
|
106
109
|
|
107
110
|
true
|
108
111
|
end
|
@@ -92,7 +92,9 @@ module ActiveElement
|
|
92
92
|
end
|
93
93
|
|
94
94
|
def model
|
95
|
-
@model ||=
|
95
|
+
@model ||= ActiveRecord::Base.descendants.find do |descendant|
|
96
|
+
descendant.name == params[:model].camelize(:upper)
|
97
|
+
end
|
96
98
|
end
|
97
99
|
|
98
100
|
def authorization
|
@@ -101,7 +103,7 @@ module ActiveElement
|
|
101
103
|
params: params,
|
102
104
|
user: controller.active_element.current_user,
|
103
105
|
search_columns: sql.search_columns.compact,
|
104
|
-
|
106
|
+
value_column: sql.value_column
|
105
107
|
)
|
106
108
|
end
|
107
109
|
|
@@ -17,6 +17,10 @@ module ActiveElement
|
|
17
17
|
def register_authorized_text_search(model:, with:, providing:)
|
18
18
|
authorized_text_searches << [model, with, providing]
|
19
19
|
end
|
20
|
+
|
21
|
+
def text_search_options(model:, with:, providing:)
|
22
|
+
{ search: { model: model.name.underscore, with: with, providing: providing } }
|
23
|
+
end
|
20
24
|
end
|
21
25
|
end
|
22
26
|
end
|
@@ -3,10 +3,11 @@
|
|
3
3
|
module ActiveElement
|
4
4
|
module Components
|
5
5
|
module Util
|
6
|
-
# Utility class for mapping an association to a linked URL (e.g. for
|
6
|
+
# Utility class for mapping an association to a linked URL display value, (e.g. for
|
7
|
+
# displaying associations in a table, form, etc.).
|
7
8
|
class AssociationMapping
|
8
|
-
def initialize(
|
9
|
-
@
|
9
|
+
def initialize(controller:, field:, record:, associated_record:, options: {})
|
10
|
+
@controller = controller
|
10
11
|
@field = field
|
11
12
|
@record = record
|
12
13
|
@associated_record = associated_record
|
@@ -15,19 +16,49 @@ module ActiveElement
|
|
15
16
|
|
16
17
|
def link_tag
|
17
18
|
verify_display_attribute
|
18
|
-
return
|
19
|
+
return associated_record.map { |value| link_to(value) } if multiple_association?
|
20
|
+
return link_to(associated_record) if single_association?
|
21
|
+
end
|
19
22
|
|
20
|
-
|
23
|
+
def relation_id # rubocop:disable Metrics/CyclomaticComplexity
|
24
|
+
case relation.macro
|
25
|
+
when :has_one
|
26
|
+
associated_record&.public_send(relation_key)
|
27
|
+
when :has_many
|
28
|
+
associated_record&.map(&relation_key)
|
29
|
+
when :belongs_to
|
30
|
+
record&.public_send(relation_key)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def relation_key
|
35
|
+
case relation.macro
|
36
|
+
when :has_one, :has_many
|
37
|
+
relation.klass.primary_key
|
38
|
+
when :belongs_to
|
39
|
+
relation.foreign_key
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def display_value(value = associated_record)
|
44
|
+
return value&.public_send(display_field) if display_field.present?
|
45
|
+
return nil if associated_model&.primary_key.blank?
|
46
|
+
|
47
|
+
value.public_send(associated_model.primary_key)
|
21
48
|
end
|
22
49
|
|
23
50
|
private
|
24
51
|
|
25
|
-
attr_reader :
|
52
|
+
attr_reader :controller, :field, :record, :associated_record, :options
|
53
|
+
|
54
|
+
def relation
|
55
|
+
@relation ||= record.class.reflect_on_association(field)
|
56
|
+
end
|
26
57
|
|
27
|
-
def associated_record_path
|
28
|
-
return nil unless
|
58
|
+
def associated_record_path(path_for)
|
59
|
+
return nil unless controller.helpers.respond_to?(path_helper)
|
29
60
|
|
30
|
-
|
61
|
+
controller.helpers.public_send(path_helper, path_for)
|
31
62
|
end
|
32
63
|
|
33
64
|
def verify_display_attribute
|
@@ -38,42 +69,66 @@ module ActiveElement
|
|
38
69
|
"`#{associated_record.class.name}.default_display_attribute`"
|
39
70
|
end
|
40
71
|
|
41
|
-
def display_value
|
42
|
-
associated_record.public_send(display_field)
|
43
|
-
end
|
44
|
-
|
45
72
|
def display_field
|
46
73
|
@display_field ||= options.fetch(:attribute) do
|
47
|
-
next
|
74
|
+
next associated_model.default_display_attribute if defined_display_attribute?
|
48
75
|
next default_display_attribute if default_display_attribute.present?
|
49
76
|
|
50
|
-
|
77
|
+
associated_model.primary_key
|
51
78
|
end
|
52
79
|
end
|
53
80
|
|
54
81
|
def defined_display_attribute?
|
55
|
-
|
82
|
+
associated_model.respond_to?(:default_display_attribute)
|
56
83
|
end
|
57
84
|
|
58
85
|
def namespace
|
59
|
-
|
86
|
+
controller.class.name.deconstantize.underscore
|
60
87
|
end
|
61
88
|
|
62
89
|
def resource_name
|
63
|
-
record.public_send(field)
|
90
|
+
record.public_send(field)&.model_name&.singular
|
64
91
|
end
|
65
92
|
|
66
93
|
def default_display_attribute
|
67
94
|
%i[name email display_name username].find do |display_field|
|
68
|
-
|
95
|
+
next true if associated_model_callable_method?(display_field.to_sym)
|
96
|
+
next true if associated_model.columns.map(&:name).map(&:to_sym).include?(display_field.to_sym)
|
97
|
+
|
98
|
+
false
|
69
99
|
end
|
70
100
|
end
|
71
101
|
|
102
|
+
def associated_model
|
103
|
+
record.association(field).klass
|
104
|
+
end
|
105
|
+
|
106
|
+
def associated_model_callable_method?(name)
|
107
|
+
return false unless associated_model.public_instance_methods.include?(name)
|
108
|
+
return false unless associated_model.public_instance_method(name).arity.zero?
|
109
|
+
|
110
|
+
true
|
111
|
+
end
|
112
|
+
|
113
|
+
def single_association?
|
114
|
+
%i[has_one belongs_to].include?(relation.macro)
|
115
|
+
end
|
116
|
+
|
117
|
+
def multiple_association?
|
118
|
+
relation.macro == :has_many
|
119
|
+
end
|
120
|
+
|
72
121
|
def path_helper
|
73
122
|
return "#{resource_name}_path" if namespace.blank?
|
74
123
|
|
75
124
|
"#{namespace}_#{resource_name}_path"
|
76
125
|
end
|
126
|
+
|
127
|
+
def link_to(value)
|
128
|
+
return display_value(value) if associated_record_path(value).blank?
|
129
|
+
|
130
|
+
controller.helpers.link_to(display_value(value), associated_record_path(value))
|
131
|
+
end
|
77
132
|
end
|
78
133
|
end
|
79
134
|
end
|
@@ -7,12 +7,21 @@ module ActiveElement
|
|
7
7
|
class DisplayValueMapping
|
8
8
|
include RecordMapping
|
9
9
|
|
10
|
+
def mapped_association_from_record
|
11
|
+
association_mapping.link_tag
|
12
|
+
end
|
13
|
+
|
10
14
|
def numeric_value
|
11
15
|
value_from_record
|
12
16
|
end
|
13
17
|
|
14
18
|
def json_value
|
15
|
-
ActiveElement.json_pretty_print(value_from_record)
|
19
|
+
return ActiveElement.json_pretty_print(value_from_record) unless component.is_a?(CollectionTable)
|
20
|
+
|
21
|
+
component.controller.render_to_string(
|
22
|
+
partial: 'active_element/components/fields/json',
|
23
|
+
locals: { value: value_from_record, field_id: ActiveElement.element_id }
|
24
|
+
)
|
16
25
|
end
|
17
26
|
|
18
27
|
def string_value
|
@@ -20,15 +29,15 @@ module ActiveElement
|
|
20
29
|
end
|
21
30
|
|
22
31
|
def datetime_value
|
23
|
-
value_from_record.strftime('%Y-%m-%d %H:%M:%S')
|
32
|
+
with_timezone_offset(value_from_record).strftime('%Y-%m-%d %H:%M:%S')
|
24
33
|
end
|
25
34
|
|
26
35
|
def time_value
|
27
|
-
value_from_record.strftime('%H:%M:%S')
|
36
|
+
with_timezone_offset(value_from_record).strftime('%H:%M:%S')
|
28
37
|
end
|
29
38
|
|
30
39
|
def date_value
|
31
|
-
value_from_record.strftime('%Y-%m-%d')
|
40
|
+
with_timezone_offset(value_from_record).strftime('%Y-%m-%d')
|
32
41
|
end
|
33
42
|
|
34
43
|
def boolean_value
|
@@ -4,13 +4,17 @@ module ActiveElement
|
|
4
4
|
module Components
|
5
5
|
module Util
|
6
6
|
# Normalizes Form `fields` parameter from various supported input formats.
|
7
|
-
class FormFieldMapping
|
7
|
+
class FormFieldMapping # rubocop:disable Metrics/ClassLength
|
8
8
|
include SecretFields
|
9
|
+
include PhoneFields
|
10
|
+
include EmailFields
|
9
11
|
|
10
|
-
def initialize(record
|
12
|
+
def initialize(record:, fields:, controller:, i18n:, search: false)
|
11
13
|
@record = record
|
12
14
|
@fields = fields
|
15
|
+
@controller = controller
|
13
16
|
@i18n = i18n
|
17
|
+
@search = search
|
14
18
|
end
|
15
19
|
|
16
20
|
def fields_with_types_and_options
|
@@ -28,7 +32,7 @@ module ActiveElement
|
|
28
32
|
|
29
33
|
private
|
30
34
|
|
31
|
-
attr_reader :fields, :i18n, :record
|
35
|
+
attr_reader :fields, :i18n, :record, :controller, :search
|
32
36
|
|
33
37
|
def normalized_field?(field)
|
34
38
|
(field.size == 3) && field.last.is_a?(Hash)
|
@@ -43,29 +47,116 @@ module ActiveElement
|
|
43
47
|
end
|
44
48
|
|
45
49
|
def field_with_default_type_and_default_options(field)
|
46
|
-
[field,
|
50
|
+
return [field, type_from_file(field).to_sym, options_from_file(field)] if file_configuration?(field)
|
51
|
+
return relation_text_search_field(field) if relation?(field) && record.present? && !search?
|
52
|
+
|
53
|
+
[field, default_type_from_model(field), default_options(field)]
|
47
54
|
end
|
48
55
|
|
49
56
|
def field_with_type(field)
|
50
|
-
[field.first, field.last,
|
57
|
+
[field.first, field.last, default_options(field)]
|
58
|
+
end
|
59
|
+
|
60
|
+
def association_mapping(field)
|
61
|
+
@association_mapping ||= AssociationMapping.new(
|
62
|
+
controller: controller,
|
63
|
+
field: field,
|
64
|
+
record: record,
|
65
|
+
associated_record: record.public_send(field)
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
def file_configuration?(field)
|
70
|
+
# XXX: json_field schema is loaded separately so we ignore file configuration here.
|
71
|
+
# This does prevent specifying an alternative field type in a config file but can still
|
72
|
+
# be defined inline if e.g. a user wants a text_area instead of a json_field:
|
73
|
+
#
|
74
|
+
# active_element.component.form fields: [:foo, :bar, [:some_json_field, :text_area]]
|
75
|
+
#
|
76
|
+
file_configuration_path(field).file? && default_type_from_model(field) != :json_field
|
77
|
+
end
|
78
|
+
|
79
|
+
def type_from_file(field)
|
80
|
+
YAML.safe_load(file_configuration_path(field).read, symbolize_names: true)
|
81
|
+
.fetch(:type, default_type_from_model(field))
|
82
|
+
end
|
83
|
+
|
84
|
+
def options_from_file(field)
|
85
|
+
YAML.safe_load(file_configuration_path(field).read, symbolize_names: true).fetch(:options, {})
|
86
|
+
end
|
87
|
+
|
88
|
+
def file_configuration_path(field)
|
89
|
+
record_field_configuration_path(field) || sti_field_configuration_path(field)
|
90
|
+
end
|
91
|
+
|
92
|
+
def record_field_configuration_path(field)
|
93
|
+
record_name = Util.record_name(record)
|
94
|
+
return nil if record_name.blank?
|
95
|
+
|
96
|
+
Rails.root.join('config/forms', record_name, "#{field}.yml")
|
97
|
+
end
|
98
|
+
|
99
|
+
def sti_record_field_configuration_path(field)
|
100
|
+
sti_record_name = Util.sti_record_name(record)
|
101
|
+
return nil if sti_record_name.blank?
|
102
|
+
|
103
|
+
Rails.root.join('config/forms', sti_record_name, "#{field}.yml")
|
51
104
|
end
|
52
105
|
|
53
106
|
def default_type_from_model(field)
|
54
107
|
return default_field_type(field) if record.blank?
|
108
|
+
return default_field_type(field) if column(field).blank?
|
109
|
+
|
110
|
+
default_type_from_column_type(field, column(field).type)
|
111
|
+
end
|
112
|
+
|
113
|
+
def column(field)
|
114
|
+
model&.columns&.find { |model_column| model_column.name.to_s == field.to_s }
|
115
|
+
end
|
116
|
+
|
117
|
+
def model
|
118
|
+
record&.class || controller.controller_name.classify.safe_constantize
|
119
|
+
end
|
120
|
+
|
121
|
+
def relation?(field)
|
122
|
+
relation(field).present?
|
123
|
+
end
|
55
124
|
|
56
|
-
|
57
|
-
|
125
|
+
def relation(field)
|
126
|
+
model&.reflect_on_association(field)
|
127
|
+
end
|
128
|
+
|
129
|
+
def relation_text_search_field(field)
|
130
|
+
relation_model = relation(field).klass
|
131
|
+
record.public_send(field)
|
132
|
+
[field, :text_search_field,
|
133
|
+
TextSearch.text_search_options(
|
134
|
+
model: relation_model,
|
135
|
+
with: searchable_fields(field),
|
136
|
+
providing: relation_model.primary_key
|
137
|
+
).merge({ display_value: association_mapping(field).display_value })]
|
138
|
+
end
|
58
139
|
|
59
|
-
|
140
|
+
def searchable_fields(field)
|
141
|
+
Util.relation_controller(model, controller, field)
|
142
|
+
.active_element
|
143
|
+
.state
|
144
|
+
.fetch(:searchable_fields, [])
|
60
145
|
end
|
61
146
|
|
62
|
-
def default_type_from_column_type(field, column_type)
|
147
|
+
def default_type_from_column_type(field, column_type) # rubocop:disable Metrics/MethodLength
|
63
148
|
{
|
64
149
|
string: default_field_type(field),
|
65
150
|
boolean: :check_box,
|
66
151
|
json: :json_field,
|
67
152
|
jsonb: :json_field,
|
68
|
-
geometry: :text_area
|
153
|
+
geometry: :text_area,
|
154
|
+
datetime: :datetime_field,
|
155
|
+
date: :date_field,
|
156
|
+
time: :time_field,
|
157
|
+
integer: :number_field,
|
158
|
+
decimal: :number_field,
|
159
|
+
float: :number_field
|
69
160
|
}.fetch(column_type.to_sym, default_field_type(field))
|
70
161
|
end
|
71
162
|
|
@@ -94,10 +185,36 @@ module ActiveElement
|
|
94
185
|
end
|
95
186
|
|
96
187
|
def default_field_type(field)
|
188
|
+
return default_search_field_type(field) if search?
|
97
189
|
return :password_field if secret_field?(field)
|
190
|
+
return :email_field if email_field?(field)
|
191
|
+
return :phone_field if phone_field?(field)
|
192
|
+
|
193
|
+
:text_field
|
194
|
+
end
|
195
|
+
|
196
|
+
def search?
|
197
|
+
search
|
198
|
+
end
|
199
|
+
|
200
|
+
def default_search_field_type(field)
|
201
|
+
return :datetime_range_field if column(field)&.type == :datetime
|
98
202
|
|
99
203
|
:text_field
|
100
204
|
end
|
205
|
+
|
206
|
+
def default_options(field)
|
207
|
+
{ required: required?(field) }
|
208
|
+
end
|
209
|
+
|
210
|
+
def required?(field)
|
211
|
+
return false if record.blank?
|
212
|
+
return false unless record.class.respond_to?(:validators)
|
213
|
+
|
214
|
+
record.class.validators.find do |validator|
|
215
|
+
validator.kind == :presence && validator.attributes.include?(field.to_sym)
|
216
|
+
end
|
217
|
+
end
|
101
218
|
end
|
102
219
|
end
|
103
220
|
end
|
@@ -20,15 +20,15 @@ module ActiveElement
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def datetime_value
|
23
|
-
value_from_record.strftime('%Y-%m-%d %H:%M:%S')
|
23
|
+
with_timezone_offset(value_from_record).strftime('%Y-%m-%d %H:%M:%S')
|
24
24
|
end
|
25
25
|
|
26
26
|
def time_value
|
27
|
-
value_from_record.strftime('%H:%M:%S')
|
27
|
+
with_timezone_offset(value_from_record).strftime('%H:%M:%S')
|
28
28
|
end
|
29
29
|
|
30
30
|
def date_value
|
31
|
-
value_from_record.strftime('%Y-%m-%d')
|
31
|
+
with_timezone_offset(value_from_record).strftime('%Y-%m-%d')
|
32
32
|
end
|
33
33
|
|
34
34
|
def boolean_value
|
@@ -7,7 +7,7 @@ module ActiveElement
|
|
7
7
|
# descriptions, placeholders, etc. for various components in locales files.
|
8
8
|
class I18n
|
9
9
|
def self.class_name(val, plural: false)
|
10
|
-
base = val&.to_s&.underscore&.tr('
|
10
|
+
base = val&.to_s&.underscore&.tr('/', '_')
|
11
11
|
plural ? base&.pluralize : base
|
12
12
|
end
|
13
13
|
|
@@ -49,41 +49,60 @@ module ActiveElement
|
|
49
49
|
end
|
50
50
|
|
51
51
|
def mapped_value_from_record
|
52
|
-
return mapped_association_from_record if
|
52
|
+
return mapped_association_from_record if association?
|
53
53
|
return nil if value_from_record.nil?
|
54
54
|
return value_from_record unless DATABASE_TYPES.include?(column.type) || value_from_record.blank?
|
55
55
|
|
56
56
|
send("#{column.type}_value")
|
57
57
|
end
|
58
58
|
|
59
|
-
def
|
60
|
-
AssociationMapping.new(
|
61
|
-
|
59
|
+
def association_mapping
|
60
|
+
@association_mapping ||= AssociationMapping.new(
|
61
|
+
controller: component.controller,
|
62
62
|
field: field,
|
63
63
|
record: record,
|
64
64
|
associated_record: value_from_record,
|
65
65
|
options: options
|
66
|
-
)
|
66
|
+
)
|
67
67
|
end
|
68
68
|
|
69
69
|
# Override these methods as required in a class that includes this module:
|
70
70
|
|
71
|
+
def mapped_association_from_record
|
72
|
+
association_mapping.relation_id
|
73
|
+
end
|
74
|
+
|
71
75
|
def numeric_value
|
72
76
|
value_from_record
|
73
77
|
end
|
74
|
-
|
75
|
-
|
76
|
-
|
78
|
+
|
79
|
+
def integer_value
|
80
|
+
numeric_value
|
81
|
+
end
|
82
|
+
|
83
|
+
def decimal_value
|
84
|
+
numeric_value
|
85
|
+
end
|
86
|
+
|
87
|
+
def float_value
|
88
|
+
numeric_value
|
89
|
+
end
|
77
90
|
|
78
91
|
def json_value
|
79
92
|
value_from_record
|
80
93
|
end
|
81
|
-
|
94
|
+
|
95
|
+
def jsonb_value
|
96
|
+
json_value
|
97
|
+
end
|
82
98
|
|
83
99
|
def string_value
|
84
100
|
value_from_record
|
85
101
|
end
|
86
|
-
|
102
|
+
|
103
|
+
def text_value
|
104
|
+
string_value
|
105
|
+
end
|
87
106
|
|
88
107
|
def datetime_value
|
89
108
|
value_from_record
|
@@ -100,11 +119,24 @@ module ActiveElement
|
|
100
119
|
def boolean_value
|
101
120
|
value_from_record
|
102
121
|
end
|
103
|
-
|
122
|
+
|
123
|
+
def binary_value
|
124
|
+
boolean_value
|
125
|
+
end
|
104
126
|
|
105
127
|
def geometry_value
|
106
128
|
value_from_record
|
107
129
|
end
|
130
|
+
|
131
|
+
def timezone_offset
|
132
|
+
component.controller.request.cookies['timezone_offset'].to_i.minutes
|
133
|
+
end
|
134
|
+
|
135
|
+
def with_timezone_offset(val)
|
136
|
+
return val if val.blank?
|
137
|
+
|
138
|
+
val - timezone_offset
|
139
|
+
end
|
108
140
|
end
|
109
141
|
end
|
110
142
|
end
|