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
@@ -13,6 +13,10 @@ module ActiveElement
|
|
13
13
|
|
14
14
|
def path
|
15
15
|
record_path || sti_record_path
|
16
|
+
rescue NoMethodError
|
17
|
+
raise Error,
|
18
|
+
"Unable to map #{record.inspect} to a Rails route. Tried:\n" \
|
19
|
+
"#{[default_record_path, sti_record_path].compact.join("\n")}"
|
16
20
|
end
|
17
21
|
|
18
22
|
private
|
@@ -36,9 +40,20 @@ module ActiveElement
|
|
36
40
|
def record_path
|
37
41
|
return nil if record.nil?
|
38
42
|
|
39
|
-
controller.helpers.public_send(default_record_path,
|
43
|
+
controller.helpers.public_send(default_record_path, *path_arguments)
|
40
44
|
rescue NoMethodError
|
41
|
-
|
45
|
+
raise NoMethodError if sti_record_name.nil?
|
46
|
+
|
47
|
+
controller.helpers.public_send(sti_record_path, *path_arguments)
|
48
|
+
end
|
49
|
+
|
50
|
+
def path_arguments
|
51
|
+
case type
|
52
|
+
when :edit, :update, :show, :destroy
|
53
|
+
[record]
|
54
|
+
when :new, :index
|
55
|
+
[]
|
56
|
+
end
|
42
57
|
end
|
43
58
|
|
44
59
|
def default_record_path
|
@@ -46,6 +61,8 @@ module ActiveElement
|
|
46
61
|
end
|
47
62
|
|
48
63
|
def sti_record_path
|
64
|
+
return nil if sti_record_name.nil?
|
65
|
+
|
49
66
|
"#{record_path_prefix}#{namespace_prefix}#{sti_record_name}_path"
|
50
67
|
end
|
51
68
|
|
@@ -63,9 +80,9 @@ module ActiveElement
|
|
63
80
|
|
64
81
|
def record_path_prefix
|
65
82
|
case type
|
66
|
-
when :edit
|
83
|
+
when :edit
|
67
84
|
'edit_'
|
68
|
-
when :new
|
85
|
+
when :new
|
69
86
|
'new_'
|
70
87
|
end
|
71
88
|
end
|
@@ -29,18 +29,25 @@ module ActiveElement
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def self.default_record_name(record)
|
32
|
-
record.class.name.demodulize.underscore
|
32
|
+
(record.is_a?(Class) ? record.name : record.class.name).demodulize.underscore
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.relation_controller(model, controller, relation)
|
36
|
+
namespace = controller.controller_path.rpartition('/').first.presence
|
37
|
+
base = "#{model.reflect_on_association(relation).klass.name.pluralize}Controller"
|
38
|
+
return base.safe_constantize if namespace.nil?
|
39
|
+
|
40
|
+
"#{namespace.classify}::#{base}".safe_constantize || base.safe_constantize
|
33
41
|
end
|
34
42
|
|
35
43
|
def self.json_pretty_print(json)
|
36
|
-
|
37
|
-
formatter = Rouge::Formatters::HTMLLinewise.new(Rouge::Formatters::HTMLInline.new(theme))
|
44
|
+
formatter = Rouge::Formatters::HTML.new
|
38
45
|
lexer = Rouge::Lexers::JSON.new
|
39
46
|
content = JSON.pretty_generate(json.is_a?(String) ? JSON.parse(json) : json)
|
40
|
-
formatted = formatter.format(lexer.lex(content)).gsub(' ', ' ')
|
47
|
+
formatted = formatter.format(lexer.lex(content)).gsub(' ', ' ').gsub("\n", '<br/>')
|
41
48
|
# rubocop:disable Rails/OutputSafety
|
42
49
|
# TODO: Move to a template.
|
43
|
-
"<div style='font-family: monospace;'>#{formatted}</div>".html_safe
|
50
|
+
"<div class='json-highlight' style='font-family: monospace;'>#{formatted}</div>".html_safe
|
44
51
|
# rubocop:enable Rails/OutputSafety
|
45
52
|
end
|
46
53
|
end
|
@@ -2,8 +2,11 @@
|
|
2
2
|
|
3
3
|
require_relative 'components/translations'
|
4
4
|
require_relative 'components/secret_fields'
|
5
|
+
require_relative 'components/email_fields'
|
6
|
+
require_relative 'components/phone_fields'
|
5
7
|
require_relative 'components/util'
|
6
8
|
require_relative 'components/link_helpers'
|
9
|
+
require_relative 'components/navbar'
|
7
10
|
require_relative 'components/page_description'
|
8
11
|
require_relative 'components/form'
|
9
12
|
require_relative 'components/collection_table'
|
@@ -11,7 +11,7 @@ module ActiveElement
|
|
11
11
|
|
12
12
|
def process_action
|
13
13
|
Rails.logger.info("#{ActiveElement.log_tag} #{colorized_permissions_message}")
|
14
|
-
return if verified_permissions?
|
14
|
+
process_pre_render and return if verified_permissions?
|
15
15
|
|
16
16
|
warn "#{ActiveElement.log_tag} #{colorized_permissions_message}" if Rails.env.test?
|
17
17
|
return controller.redirect_to redirect_path if redirect_to_default_landing_page?
|
@@ -30,6 +30,12 @@ module ActiveElement
|
|
30
30
|
(@verified_permissions = permissions_check.permitted?)
|
31
31
|
end
|
32
32
|
|
33
|
+
def process_pre_render
|
34
|
+
PreRenderProcessors::Json.new(controller: controller).process
|
35
|
+
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
33
39
|
def redirect_path
|
34
40
|
routes.alternative_routes.first.path
|
35
41
|
end
|
@@ -72,7 +78,7 @@ module ActiveElement
|
|
72
78
|
color = if permissions_check.permitted?
|
73
79
|
:green
|
74
80
|
else
|
75
|
-
|
81
|
+
rails_component.environment == 'test' ? :yellow : :red
|
76
82
|
end
|
77
83
|
paintbrush { public_send(color, permissions_check.message) }
|
78
84
|
end
|
@@ -5,7 +5,7 @@ module ActiveElement
|
|
5
5
|
# Encapsulates core functionality such as `authenticate_with`, `permit_action`, and `component`
|
6
6
|
# without polluting application controller namespace.
|
7
7
|
class ControllerInterface
|
8
|
-
attr_reader :missing_template_store, :current_user
|
8
|
+
attr_reader :missing_template_store, :current_user, :assigned_editable_fields
|
9
9
|
|
10
10
|
@state = {}
|
11
11
|
|
@@ -25,6 +25,22 @@ module ActiveElement
|
|
25
25
|
@authorize
|
26
26
|
end
|
27
27
|
|
28
|
+
def listable_fields(*args)
|
29
|
+
state[:listable_fields] = args.map(&:to_sym)
|
30
|
+
end
|
31
|
+
|
32
|
+
def viewable_fields(*args)
|
33
|
+
state[:viewable_fields] = args.map(&:to_sym)
|
34
|
+
end
|
35
|
+
|
36
|
+
def editable_fields(*args)
|
37
|
+
state[:editable_fields] = args.map(&:to_sym)
|
38
|
+
end
|
39
|
+
|
40
|
+
def searchable_fields(*args)
|
41
|
+
state[:searchable_fields] = args.map(&:to_sym)
|
42
|
+
end
|
43
|
+
|
28
44
|
def application_name
|
29
45
|
RailsComponent.new(::Rails).application_name
|
30
46
|
end
|
@@ -38,6 +54,32 @@ module ActiveElement
|
|
38
54
|
state[:authorizor] = block
|
39
55
|
end
|
40
56
|
|
57
|
+
def sign_out_with(method: :get, &block)
|
58
|
+
state[:sign_out_method] = method
|
59
|
+
state[:sign_out_path] = block
|
60
|
+
end
|
61
|
+
|
62
|
+
def sign_out_path
|
63
|
+
state[:sign_out_path]&.call
|
64
|
+
end
|
65
|
+
|
66
|
+
def sign_out_method
|
67
|
+
state[:sign_out_method]
|
68
|
+
end
|
69
|
+
|
70
|
+
def sign_in_with(method: :get, &block)
|
71
|
+
state[:sign_in_method] = method
|
72
|
+
state[:sign_in_path] = block
|
73
|
+
end
|
74
|
+
|
75
|
+
def sign_in_path
|
76
|
+
state[:sign_in_path]&.call
|
77
|
+
end
|
78
|
+
|
79
|
+
def sign_in_method
|
80
|
+
state[:sign_in_method]
|
81
|
+
end
|
82
|
+
|
41
83
|
def authenticate
|
42
84
|
authenticator&.call
|
43
85
|
@current_user = state[:authorizor]&.call
|
@@ -66,6 +108,10 @@ module ActiveElement
|
|
66
108
|
raise ArgumentError, 'Attempted to use ActiveElement component from a controller class method.'
|
67
109
|
end
|
68
110
|
|
111
|
+
def state
|
112
|
+
self.class.state[controller_class]
|
113
|
+
end
|
114
|
+
|
69
115
|
private
|
70
116
|
|
71
117
|
attr_reader :controller_class, :controller_instance
|
@@ -73,9 +119,5 @@ module ActiveElement
|
|
73
119
|
def initialize_state
|
74
120
|
self.class.state[controller_class] ||= { permissions: [], authenticator: nil }
|
75
121
|
end
|
76
|
-
|
77
|
-
def state
|
78
|
-
self.class.state[controller_class]
|
79
|
-
end
|
80
122
|
end
|
81
123
|
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveElement
|
4
|
+
# Encapsulation of all logic performed for default controller actions when no action is defined
|
5
|
+
# by the current controller.
|
6
|
+
class DefaultController
|
7
|
+
def initialize(controller:)
|
8
|
+
@controller = controller
|
9
|
+
end
|
10
|
+
|
11
|
+
def index
|
12
|
+
controller.render 'active_element/default_views/index',
|
13
|
+
locals: {
|
14
|
+
collection: collection,
|
15
|
+
search_filters: default_text_search.search_filters
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
def show
|
20
|
+
controller.render 'active_element/default_views/show', locals: { record: record }
|
21
|
+
end
|
22
|
+
|
23
|
+
def new
|
24
|
+
controller.render 'active_element/default_views/new', locals: { record: model.new, namespace: namespace }
|
25
|
+
end
|
26
|
+
|
27
|
+
def create # rubocop:disable Metrics/AbcSize
|
28
|
+
new_record = model.new(default_record_params.params)
|
29
|
+
# Ensure associations are applied:
|
30
|
+
if new_record.save && new_record.reload.update(default_record_params.params)
|
31
|
+
controller.flash.notice = "#{new_record.model_name.to_s.titleize} created successfully."
|
32
|
+
controller.redirect_to record_path(new_record, :show).path
|
33
|
+
else
|
34
|
+
controller.flash.now.alert = "Failed to create #{model.name.to_s.titleize}."
|
35
|
+
controller.render 'active_element/default_views/new', locals: { record: new_record, namespace: namespace }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def edit
|
40
|
+
controller.render 'active_element/default_views/edit', locals: { record: record, namespace: namespace }
|
41
|
+
end
|
42
|
+
|
43
|
+
def update # rubocop:disable Metrics/AbcSize
|
44
|
+
if record.update(default_record_params.params)
|
45
|
+
controller.flash.notice = "#{record.model_name.to_s.titleize} updated successfully."
|
46
|
+
controller.redirect_to record_path(record, :show).path
|
47
|
+
else
|
48
|
+
controller.flash.now.alert = "Failed to update #{model.name.to_s.titleize}."
|
49
|
+
controller.render 'active_element/default_views/edit', locals: { record: record, namespace: namespace }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def destroy
|
54
|
+
record.destroy
|
55
|
+
controller.flash.notice = "Deleted #{record.model_name.to_s.titleize}."
|
56
|
+
controller.redirect_to record_path(model, :index).path
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
attr_reader :controller
|
62
|
+
|
63
|
+
def default_record_params
|
64
|
+
@default_record_params ||= ActiveElement::DefaultRecordParams.new(controller: controller, model: model)
|
65
|
+
end
|
66
|
+
|
67
|
+
def default_text_search
|
68
|
+
@default_text_search ||= ActiveElement::DefaultTextSearch.new(controller: controller, model: model)
|
69
|
+
end
|
70
|
+
|
71
|
+
def record_path(record, type = nil)
|
72
|
+
ActiveElement::Components::Util::RecordPath.new(record: record, controller: controller, type: type)
|
73
|
+
end
|
74
|
+
|
75
|
+
def namespace
|
76
|
+
controller.controller_path.rpartition('/').first.presence&.to_sym
|
77
|
+
end
|
78
|
+
|
79
|
+
def model
|
80
|
+
controller.controller_name.classify.constantize
|
81
|
+
end
|
82
|
+
|
83
|
+
def record
|
84
|
+
@record ||= model.find(controller.params[:id])
|
85
|
+
end
|
86
|
+
|
87
|
+
def collection
|
88
|
+
return model.all unless default_text_search.text_search?
|
89
|
+
|
90
|
+
model.left_outer_joins(default_text_search.search_relations).where(*default_text_search.text_search)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveElement
|
4
|
+
# Provides params for ActiveRecord models when using the default boilerplate controller
|
5
|
+
# actions. Navigates input parameters and maps them to appropriate relations as needed.
|
6
|
+
class DefaultRecordParams
|
7
|
+
def initialize(controller:, model:)
|
8
|
+
@controller = controller
|
9
|
+
@model = model
|
10
|
+
end
|
11
|
+
|
12
|
+
def params
|
13
|
+
with_transformed_relations(
|
14
|
+
controller.params.require(controller.controller_name.singularize)
|
15
|
+
.permit(controller.active_element.state.fetch(:editable_fields, []))
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_reader :controller, :model
|
22
|
+
|
23
|
+
def with_transformed_relations(params)
|
24
|
+
params.to_h.to_h do |key, value|
|
25
|
+
next [key, value] unless relation?(key)
|
26
|
+
|
27
|
+
relation_param(key, value)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def relation_param(key, value)
|
32
|
+
case relation(key).macro
|
33
|
+
when :belongs_to
|
34
|
+
belongs_to_param(key, value)
|
35
|
+
when :has_one
|
36
|
+
has_one_param(key, value)
|
37
|
+
when :has_many
|
38
|
+
has_many_param(key, value)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def belongs_to_param(key, value)
|
43
|
+
[relation(key).foreign_key, value]
|
44
|
+
end
|
45
|
+
|
46
|
+
def has_one_param(key, value) # rubocop:disable Naming/PredicateName
|
47
|
+
[relation(key).name, relation(key).klass.find_by(relation(key).klass.primary_key => value)]
|
48
|
+
end
|
49
|
+
|
50
|
+
def has_many_param(key, _value) # rubocop:disable Naming/PredicateName
|
51
|
+
[relation(key).name, relation(key).klass.where(relation(key).klass.primary_key => relation(key).value)]
|
52
|
+
end
|
53
|
+
|
54
|
+
def relation?(attribute)
|
55
|
+
relation(attribute.to_sym).present?
|
56
|
+
end
|
57
|
+
|
58
|
+
def relation(attribute)
|
59
|
+
model.reflect_on_association(attribute.to_sym)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveElement
|
4
|
+
# Full text search querying for DefaultController, provides full text search filters for all
|
5
|
+
# controllers with configured searchable fields. Includes support for querying across relations.
|
6
|
+
class DefaultTextSearch
|
7
|
+
def initialize(controller:, model:)
|
8
|
+
@controller = controller
|
9
|
+
@model = model
|
10
|
+
end
|
11
|
+
|
12
|
+
def search_filters
|
13
|
+
@search_filters ||= controller.params.permit(*searchable_fields).transform_values do |value|
|
14
|
+
value.try(:compact_blank) || value
|
15
|
+
end.compact_blank
|
16
|
+
end
|
17
|
+
|
18
|
+
def text_search?
|
19
|
+
search_filters.present?
|
20
|
+
end
|
21
|
+
|
22
|
+
def text_search
|
23
|
+
conditions = search_filters.to_h.map do |key, value|
|
24
|
+
next relation_matches(key, value) if relation?(key)
|
25
|
+
next datetime_between(key, value) if datetime?(key)
|
26
|
+
|
27
|
+
model.arel_table[key].matches("#{value}%")
|
28
|
+
end
|
29
|
+
conditions[1..].reduce(conditions.first) do |accumulated, condition|
|
30
|
+
accumulated.and(condition)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def search_relations
|
35
|
+
search_filters.to_h.keys.map { |key| relation?(key) ? key.to_sym : nil }.compact
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
attr_reader :controller, :model
|
41
|
+
|
42
|
+
def searchable_fields
|
43
|
+
base_fields = controller.active_element.state.fetch(:searchable_fields, [])
|
44
|
+
base_fields.map do |field|
|
45
|
+
next field unless field.to_s.end_with?('_at')
|
46
|
+
|
47
|
+
{ field => %i[from to] }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def noop
|
52
|
+
Arel::Nodes::True.new.eq(Arel::Nodes::True.new)
|
53
|
+
end
|
54
|
+
|
55
|
+
def datetime?(key)
|
56
|
+
model.columns.find { |column| column.name.to_s == key.to_s }&.type == :datetime
|
57
|
+
end
|
58
|
+
|
59
|
+
def datetime_between(key, value)
|
60
|
+
return noop if value[:from].blank? && value[:to].blank?
|
61
|
+
|
62
|
+
model.arel_table[key].between(range_begin(value)...range_end(value))
|
63
|
+
end
|
64
|
+
|
65
|
+
def range_begin(value)
|
66
|
+
value[:from].present? ? Time.zone.parse(value[:from]) + timezone_offset : -Float::INFINITY
|
67
|
+
end
|
68
|
+
|
69
|
+
def range_end
|
70
|
+
value[:to].present? ? Time.zone.parse(value[:to]) + timezone_offset : Float::INFINITY
|
71
|
+
end
|
72
|
+
|
73
|
+
def timezone_offset
|
74
|
+
controller.request.cookies['timezone_offset'].to_i.minutes
|
75
|
+
end
|
76
|
+
|
77
|
+
def relation_matches(key, value)
|
78
|
+
fields = searchable_relation_fields(key)
|
79
|
+
relation_model = relation(key).klass
|
80
|
+
fields.select! do |field|
|
81
|
+
relation_model.columns.find { |column| column.name.to_s == field.to_s }&.type == :string
|
82
|
+
end
|
83
|
+
|
84
|
+
return noop if fields.empty?
|
85
|
+
|
86
|
+
relation_conditions(fields, value, relation_model)
|
87
|
+
end
|
88
|
+
|
89
|
+
def relation_conditions(fields, value, relation_model)
|
90
|
+
fields[1..].reduce(relation_model.arel_table[fields.first].matches("#{value}%")) do |condition, field|
|
91
|
+
condition.or(relation_model.arel_table[field].matches("#{value}%"))
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def searchable_relation_fields(key)
|
96
|
+
Components::Util.relation_controller(model, controller, key)
|
97
|
+
&.active_element
|
98
|
+
&.state
|
99
|
+
&.fetch(:searchable_fields, []) || []
|
100
|
+
end
|
101
|
+
|
102
|
+
def relation?(attribute)
|
103
|
+
relation(attribute.to_sym).present?
|
104
|
+
end
|
105
|
+
|
106
|
+
def relation(attribute)
|
107
|
+
model.reflect_on_association(attribute.to_sym)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveElement
|
4
|
+
# Generates a schema for a JSON form field based on values stored in the database.
|
5
|
+
class JsonFieldSchema
|
6
|
+
def initialize(table:, column:)
|
7
|
+
@table = table
|
8
|
+
@column = column
|
9
|
+
end
|
10
|
+
|
11
|
+
def schema
|
12
|
+
data.map { |datum| structure(datum) }
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
attr_reader :table, :column, :initial_structure
|
18
|
+
|
19
|
+
def data
|
20
|
+
@data ||= ActiveRecord::Base.connection
|
21
|
+
.execute("select #{column} from #{table}")
|
22
|
+
.pluck(column)
|
23
|
+
.map { |datum| JSON.parse(datum) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def structure(datum)
|
27
|
+
{
|
28
|
+
type: schema_type(datum),
|
29
|
+
shape: schema_shape(datum)&.compact,
|
30
|
+
fields: schema_fields(datum)&.compact
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def schema_type(val)
|
35
|
+
case val
|
36
|
+
when Hash
|
37
|
+
'object'
|
38
|
+
when Array
|
39
|
+
'array'
|
40
|
+
when String
|
41
|
+
'string'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def schema_shape(val)
|
46
|
+
return nil unless %w[array object].include?(schema_type(val))
|
47
|
+
return val.map { |item| structure(item).compact } if schema_type(val) == 'array'
|
48
|
+
return val.map { |_key, value| structure(value).compact } if schema_type(val) == 'object'
|
49
|
+
|
50
|
+
{ type: schema_type(val) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def schema_fields(val)
|
54
|
+
return nil unless schema_type(val) == 'object'
|
55
|
+
|
56
|
+
val.map { |key, value| { name: key }.merge(structure(value)).compact }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveElement
|
4
|
+
module PreRenderProcessors
|
5
|
+
# Selects fields from `__json_fields` param created by `Components::JsonField` and parses
|
6
|
+
# each field's JSON data back into request params to allow for transparent JSON data receipt.
|
7
|
+
# All params are permitted and converted to a Hash to allow them to be modified before
|
8
|
+
# converting back to ActionController::Params to avoid disrupting the Rails request flow.
|
9
|
+
class Json
|
10
|
+
def initialize(controller:)
|
11
|
+
@controller = controller
|
12
|
+
end
|
13
|
+
|
14
|
+
def process
|
15
|
+
return if json_fields.blank?
|
16
|
+
|
17
|
+
process_json_fields
|
18
|
+
delete_meta_params
|
19
|
+
rebuild_action_controller_parameters
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
attr_reader :controller
|
25
|
+
|
26
|
+
def process_json_fields
|
27
|
+
json_fields.zip(json_values).each do |field, value|
|
28
|
+
*nested_keys, field_key = field.split('.')
|
29
|
+
param = nested_keys.reduce(permitted_params) { |params, key| params[key] }
|
30
|
+
schema = schema_for(nested_keys + [field_key])
|
31
|
+
param[field_key] = coerced_with_default(value, schema)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def coerced_with_default(value, schema)
|
36
|
+
return coerced_value(JSON.parse(value), schema: schema) unless value == ''
|
37
|
+
|
38
|
+
{ 'array' => [], 'object' => {} }.fetch(schema['type'])
|
39
|
+
end
|
40
|
+
|
41
|
+
def delete_meta_params
|
42
|
+
permitted_params.delete('__json_fields')
|
43
|
+
permitted_params.delete('__json_field_schemas')
|
44
|
+
end
|
45
|
+
|
46
|
+
def rebuild_action_controller_parameters
|
47
|
+
controller.params = ActionController::Parameters.new(permitted_params)
|
48
|
+
end
|
49
|
+
|
50
|
+
def json_fields
|
51
|
+
controller.params['__json_fields']
|
52
|
+
end
|
53
|
+
|
54
|
+
def json_values
|
55
|
+
json_fields.map do |json_field|
|
56
|
+
json_field.split('.').reduce(controller.params) { |params, field| params[field] }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def permitted_params
|
61
|
+
@permitted_params ||= controller.params.permit!.to_h
|
62
|
+
end
|
63
|
+
|
64
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
65
|
+
def coerced_value(val, schema:)
|
66
|
+
return val if val.nil?
|
67
|
+
|
68
|
+
case schema['type']
|
69
|
+
when 'array'
|
70
|
+
val.map { |item| coerced_value(item, schema: schema['shape']) }
|
71
|
+
when 'object'
|
72
|
+
val.to_h { |key, value| [key, coerced_value(value, schema: schema_field(schema, key))] }
|
73
|
+
when 'string', 'boolean', 'time'
|
74
|
+
val
|
75
|
+
when 'float'
|
76
|
+
Float(val)
|
77
|
+
when 'integer'
|
78
|
+
Integer(val)
|
79
|
+
when 'decimal'
|
80
|
+
BigDecimal(val)
|
81
|
+
when 'datetime'
|
82
|
+
DateTime.parse(val)
|
83
|
+
when 'date'
|
84
|
+
Date.parse(val)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
88
|
+
|
89
|
+
def schema_for(path)
|
90
|
+
JSON.parse(path.reduce(permitted_params['__json_field_schemas']) { |schema, key| schema[key] })
|
91
|
+
end
|
92
|
+
|
93
|
+
def schema_field(schema, key)
|
94
|
+
schema['shape']['fields'].find { |each_field| each_field['name'] == key }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'pre_render_processors/json'
|
4
|
+
|
5
|
+
module ActiveElement
|
6
|
+
# Collection of processors called before the controller flow is handed back to the host
|
7
|
+
# application, i.e. before actions within the main ActiveElement before action, e.g. used for
|
8
|
+
# processing JSON fields.
|
9
|
+
module PreRenderProcessors
|
10
|
+
end
|
11
|
+
end
|
data/lib/active_element/route.rb
CHANGED
@@ -36,6 +36,7 @@ module ActiveElement
|
|
36
36
|
def primary?
|
37
37
|
return false if rails_non_index_action?
|
38
38
|
return false unless resourceless_get_request?
|
39
|
+
return false if excluded_ancestor?
|
39
40
|
|
40
41
|
true
|
41
42
|
end
|
@@ -93,6 +94,17 @@ module ActiveElement
|
|
93
94
|
end
|
94
95
|
end
|
95
96
|
|
97
|
+
def excluded_ancestor?
|
98
|
+
ancestors = controller.class.ancestors.map(&:name)
|
99
|
+
excluded_ancestors.any? { |excluded_ancestor| ancestors.include?(excluded_ancestor) }
|
100
|
+
end
|
101
|
+
|
102
|
+
def excluded_ancestors
|
103
|
+
# This will likely end up a config setting, for now we exclude Devise so its controllers
|
104
|
+
# don't appear in the Navbar.
|
105
|
+
%w[DeviseController]
|
106
|
+
end
|
107
|
+
|
96
108
|
def permitted_action?
|
97
109
|
permissions_check.permitted?
|
98
110
|
rescue UnprotectedRouteError
|
@@ -33,7 +33,8 @@ module ActiveElement
|
|
33
33
|
|
34
34
|
def available_routes
|
35
35
|
@available_routes ||= descendants_with_permissions.map do |descendant, required_permissions|
|
36
|
-
descendant.public_methods(false)
|
36
|
+
action_methods = descendant.public_methods(false)
|
37
|
+
([:index] + action_methods).uniq.map do |action|
|
37
38
|
route(descendant, action, required_permissions)
|
38
39
|
end
|
39
40
|
end.flatten.compact.select(&:rails_route?).sort
|