active_element 0.0.10 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +12 -2
  3. data/.strong_versions.yml +1 -0
  4. data/Gemfile +5 -0
  5. data/Gemfile.lock +115 -75
  6. data/Makefile +10 -0
  7. data/active_element.gemspec +1 -1
  8. data/app/assets/javascripts/active_element/application.js +1 -0
  9. data/app/assets/javascripts/active_element/form.js +16 -32
  10. data/app/assets/javascripts/active_element/json_field.js +391 -135
  11. data/app/assets/javascripts/active_element/setup.js +13 -8
  12. data/app/assets/javascripts/active_element/text_search_field.js +38 -27
  13. data/app/assets/javascripts/active_element/theme.js +1 -1
  14. data/app/assets/javascripts/active_element/timezones.js +6 -0
  15. data/app/assets/stylesheets/active_element/_dark.scss +86 -0
  16. data/app/assets/stylesheets/active_element/_variables.scss +2 -1
  17. data/app/assets/stylesheets/active_element/application.scss +166 -33
  18. data/app/controllers/active_element/application_controller.rb +5 -0
  19. data/app/controllers/concerns/active_element/default_controller_actions.rb +38 -0
  20. data/app/views/active_element/_user.html.erb +20 -0
  21. data/app/views/active_element/components/fields/_json.html.erb +24 -0
  22. data/app/views/active_element/components/form/_check_box.html.erb +1 -0
  23. data/app/views/active_element/components/form/_check_boxes.html.erb +1 -1
  24. data/app/views/active_element/components/form/_datetime_range_field.html.erb +14 -0
  25. data/app/views/active_element/components/form/_field.html.erb +10 -7
  26. data/app/views/active_element/components/form/_generic_field.html.erb +1 -0
  27. data/app/views/active_element/components/form/_json.html.erb +10 -2
  28. data/app/views/active_element/components/form/_label.html.erb +12 -1
  29. data/app/views/active_element/components/form/_select.html.erb +4 -1
  30. data/app/views/active_element/components/form/_summary.html.erb +11 -1
  31. data/app/views/active_element/components/form/_templates.html.erb +42 -24
  32. data/app/views/active_element/components/form/_text_area.html.erb +2 -1
  33. data/app/views/active_element/components/form/_text_search.html.erb +8 -4
  34. data/app/views/active_element/components/form.html.erb +20 -17
  35. data/app/views/active_element/components/json.html.erb +1 -0
  36. data/app/views/active_element/components/navbar.html.erb +26 -0
  37. data/app/views/active_element/components/table/_collection_row.html.erb +2 -1
  38. data/app/views/active_element/components/table/_field.html.erb +8 -0
  39. data/app/views/active_element/components/table/_ungrouped_collection.html.erb +1 -0
  40. data/app/views/active_element/components/table/collection.html.erb +1 -1
  41. data/app/views/active_element/components/table/item.html.erb +6 -4
  42. data/app/views/active_element/default_views/edit.html.erb +5 -0
  43. data/app/views/active_element/default_views/forbidden.html.erb +7 -0
  44. data/app/views/active_element/default_views/index.html.erb +15 -0
  45. data/app/views/active_element/default_views/new.html.erb +4 -0
  46. data/app/views/active_element/default_views/show.html.erb +7 -0
  47. data/app/views/active_element/navbar/_menu.html.erb +1 -30
  48. data/app/views/active_element/theme/_select.html.erb +1 -1
  49. data/app/views/layouts/active_element.html.erb +16 -1
  50. data/config/brakeman.ignore +48 -0
  51. data/config/locales/en.yml +3 -0
  52. data/example_app/.gitattributes +7 -0
  53. data/example_app/.gitignore +35 -0
  54. data/example_app/.ruby-version +1 -0
  55. data/example_app/Gemfile +34 -0
  56. data/example_app/Gemfile.lock +296 -0
  57. data/example_app/README.md +24 -0
  58. data/example_app/Rakefile +6 -0
  59. data/example_app/app/assets/config/manifest.js +4 -0
  60. data/example_app/app/assets/images/.keep +0 -0
  61. data/example_app/app/assets/stylesheets/application.css +15 -0
  62. data/example_app/app/channels/application_cable/channel.rb +4 -0
  63. data/example_app/app/channels/application_cable/connection.rb +4 -0
  64. data/example_app/app/controllers/application_controller.rb +12 -0
  65. data/example_app/app/controllers/concerns/.keep +0 -0
  66. data/example_app/app/controllers/pets_controller.rb +7 -0
  67. data/example_app/app/controllers/users_controller.rb +7 -0
  68. data/example_app/app/helpers/application_helper.rb +2 -0
  69. data/example_app/app/javascript/application.js +3 -0
  70. data/example_app/app/javascript/controllers/application.js +9 -0
  71. data/example_app/app/javascript/controllers/hello_controller.js +7 -0
  72. data/example_app/app/javascript/controllers/index.js +11 -0
  73. data/example_app/app/jobs/application_job.rb +7 -0
  74. data/example_app/app/mailers/application_mailer.rb +4 -0
  75. data/example_app/app/models/application_record.rb +3 -0
  76. data/example_app/app/models/concerns/.keep +0 -0
  77. data/example_app/app/models/pet.rb +3 -0
  78. data/example_app/app/models/user.rb +8 -0
  79. data/example_app/app/views/layouts/application.html.erb +16 -0
  80. data/example_app/app/views/layouts/mailer.html.erb +13 -0
  81. data/example_app/app/views/layouts/mailer.text.erb +1 -0
  82. data/example_app/app/views/pets/index.html.erb +3 -0
  83. data/example_app/app/views/users/show.html.erb +3 -0
  84. data/example_app/bin/bundle +109 -0
  85. data/example_app/bin/importmap +4 -0
  86. data/example_app/bin/rails +4 -0
  87. data/example_app/bin/rake +4 -0
  88. data/example_app/bin/setup +33 -0
  89. data/example_app/config/application.rb +22 -0
  90. data/example_app/config/boot.rb +4 -0
  91. data/example_app/config/cable.yml +10 -0
  92. data/example_app/config/credentials.yml.enc +1 -0
  93. data/example_app/config/database.yml +25 -0
  94. data/example_app/config/environment.rb +5 -0
  95. data/example_app/config/environments/development.rb +70 -0
  96. data/example_app/config/environments/production.rb +93 -0
  97. data/example_app/config/environments/test.rb +60 -0
  98. data/example_app/config/importmap.rb +7 -0
  99. data/example_app/config/initializers/assets.rb +12 -0
  100. data/example_app/config/initializers/content_security_policy.rb +25 -0
  101. data/example_app/config/initializers/devise.rb +16 -0
  102. data/example_app/config/initializers/filter_parameter_logging.rb +8 -0
  103. data/example_app/config/initializers/inflections.rb +16 -0
  104. data/example_app/config/initializers/permissions_policy.rb +11 -0
  105. data/example_app/config/locales/devise.en.yml +65 -0
  106. data/example_app/config/locales/en.yml +33 -0
  107. data/example_app/config/puma.rb +43 -0
  108. data/example_app/config/routes.rb +8 -0
  109. data/example_app/config/storage.yml +34 -0
  110. data/example_app/config.ru +6 -0
  111. data/example_app/db/migrate/20230616210539_create_pet.rb +12 -0
  112. data/example_app/db/migrate/20230616211328_devise_create_users.rb +46 -0
  113. data/example_app/db/schema.rb +37 -0
  114. data/example_app/db/seeds.rb +33 -0
  115. data/example_app/lib/assets/.keep +0 -0
  116. data/example_app/lib/tasks/.keep +0 -0
  117. data/example_app/log/.keep +0 -0
  118. data/example_app/public/404.html +67 -0
  119. data/example_app/public/422.html +67 -0
  120. data/example_app/public/500.html +66 -0
  121. data/example_app/public/apple-touch-icon-precomposed.png +0 -0
  122. data/example_app/public/apple-touch-icon.png +0 -0
  123. data/example_app/public/favicon.ico +0 -0
  124. data/example_app/public/robots.txt +1 -0
  125. data/example_app/storage/.keep +0 -0
  126. data/example_app/test/application_system_test_case.rb +5 -0
  127. data/example_app/test/channels/application_cable/connection_test.rb +11 -0
  128. data/example_app/test/controllers/.keep +0 -0
  129. data/example_app/test/fixtures/files/.keep +0 -0
  130. data/example_app/test/fixtures/users.yml +11 -0
  131. data/example_app/test/helpers/.keep +0 -0
  132. data/example_app/test/integration/.keep +0 -0
  133. data/example_app/test/mailers/.keep +0 -0
  134. data/example_app/test/models/.keep +0 -0
  135. data/example_app/test/models/user_test.rb +7 -0
  136. data/example_app/test/system/.keep +0 -0
  137. data/example_app/test/test_helper.rb +13 -0
  138. data/example_app/tmp/.keep +0 -0
  139. data/example_app/tmp/pids/.keep +0 -0
  140. data/example_app/tmp/storage/.keep +0 -0
  141. data/example_app/vendor/.keep +0 -0
  142. data/example_app/vendor/javascript/.keep +0 -0
  143. data/lib/active_element/component.rb +9 -2
  144. data/lib/active_element/components/collection_table.rb +9 -2
  145. data/lib/active_element/components/email_fields.rb +14 -0
  146. data/lib/active_element/components/form.rb +48 -17
  147. data/lib/active_element/components/navbar.rb +64 -0
  148. data/lib/active_element/components/phone_fields.rb +14 -0
  149. data/lib/active_element/components/text_search/authorization.rb +9 -6
  150. data/lib/active_element/components/text_search/component.rb +4 -2
  151. data/lib/active_element/components/text_search.rb +13 -0
  152. data/lib/active_element/components/util/association_mapping.rb +74 -19
  153. data/lib/active_element/components/util/display_value_mapping.rb +13 -4
  154. data/lib/active_element/components/util/form_field_mapping.rb +139 -10
  155. data/lib/active_element/components/util/form_value_mapping.rb +3 -3
  156. data/lib/active_element/components/util/i18n.rb +1 -1
  157. data/lib/active_element/components/util/numeric_field.rb +73 -0
  158. data/lib/active_element/components/util/record_mapping.rb +43 -11
  159. data/lib/active_element/components/util/record_path.rb +21 -4
  160. data/lib/active_element/components/util.rb +13 -5
  161. data/lib/active_element/components.rb +3 -0
  162. data/lib/active_element/controller_action.rb +8 -2
  163. data/lib/active_element/controller_interface.rb +56 -18
  164. data/lib/active_element/controller_state.rb +44 -0
  165. data/lib/active_element/default_controller.rb +137 -0
  166. data/lib/active_element/default_record_params.rb +62 -0
  167. data/lib/active_element/default_search.rb +110 -0
  168. data/lib/active_element/json_field_schema.rb +59 -0
  169. data/lib/active_element/pre_render_processors/json.rb +98 -0
  170. data/lib/active_element/pre_render_processors.rb +11 -0
  171. data/lib/active_element/route.rb +12 -0
  172. data/lib/active_element/routes.rb +2 -1
  173. data/lib/active_element/version.rb +1 -1
  174. data/lib/active_element.rb +15 -32
  175. data/lib/tasks/active_element.rake +12 -1
  176. data/rspec-documentation/_head.html.erb +34 -0
  177. data/rspec-documentation/pages/000-Introduction.md +18 -0
  178. data/rspec-documentation/pages/005-Setup.md +75 -0
  179. data/rspec-documentation/pages/010-Components/Form Fields/Check Boxes.md +1 -0
  180. data/rspec-documentation/pages/010-Components/Form Fields/JSON/Controller Params.md +97 -0
  181. data/rspec-documentation/pages/010-Components/Form Fields/JSON/Schema.md +283 -0
  182. data/rspec-documentation/pages/010-Components/Form Fields/JSON/Types.md +36 -0
  183. data/rspec-documentation/pages/010-Components/Form Fields/JSON.md +70 -0
  184. data/rspec-documentation/pages/010-Components/Form Fields/Text Search.md +133 -0
  185. data/rspec-documentation/pages/010-Components/Form Fields.md +46 -0
  186. data/rspec-documentation/pages/010-Components/Forms.md +44 -0
  187. data/rspec-documentation/pages/010-Components/JSON Data.md +23 -0
  188. data/rspec-documentation/pages/010-Components/Navbar.md +56 -0
  189. data/rspec-documentation/pages/010-Components/Page Section Title.md +13 -0
  190. data/rspec-documentation/pages/010-Components/Page Subtitle.md +11 -0
  191. data/rspec-documentation/pages/010-Components/Page Title.md +11 -0
  192. data/rspec-documentation/pages/010-Components/Tables/Collection Table.md +29 -0
  193. data/rspec-documentation/pages/010-Components/Tables/Item Table.md +18 -0
  194. data/rspec-documentation/pages/010-Components/Tables/Options.md +19 -0
  195. data/rspec-documentation/pages/010-Components/Tables.md +29 -0
  196. data/rspec-documentation/pages/010-Components.md +15 -0
  197. data/rspec-documentation/pages/020-Access Control/010-Authentication.md +20 -0
  198. data/rspec-documentation/pages/020-Access Control/020-Authorization/Environments.md +9 -0
  199. data/rspec-documentation/pages/020-Access Control/020-Authorization/Permissions/Custom Routes.md +41 -0
  200. data/rspec-documentation/pages/020-Access Control/020-Authorization/Permissions.md +58 -0
  201. data/rspec-documentation/pages/020-Access Control/020-Authorization/Setup.md +27 -0
  202. data/rspec-documentation/pages/020-Access Control/020-Authorization.md +11 -0
  203. data/rspec-documentation/pages/020-Access Control.md +31 -0
  204. data/rspec-documentation/pages/040-Decorators/Inline Decorators.md +24 -0
  205. data/rspec-documentation/pages/040-Decorators/View Decorators.md +55 -0
  206. data/rspec-documentation/pages/040-Decorators.md +12 -0
  207. data/rspec-documentation/pages/300-Alternatives.md +21 -0
  208. data/rspec-documentation/pages/900-License.md +11 -0
  209. data/rspec-documentation/spec_helper.rb +53 -16
  210. data/rspec-documentation/support.rb +84 -0
  211. metadata +159 -14
  212. data/rspec-documentation/pages/Components/Forms.md +0 -1
  213. data/rspec-documentation/pages/Components/Tables.md +0 -47
  214. data/rspec-documentation/pages/Components.md +0 -1
  215. data/rspec-documentation/pages/Decorators/Inline Decorators.md +0 -1
  216. data/rspec-documentation/pages/Decorators/View Decorators.md +0 -1
  217. data/rspec-documentation/pages/Index.md +0 -3
  218. data/rspec-documentation/pages/Util/I18n.md +0 -1
  219. /data/rspec-documentation/pages/{Components → 010-Components}/Tabs.md +0 -0
@@ -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, fields, i18n)
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, default_type_from_model(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
124
+
125
+ def relation(field)
126
+ model&.reflect_on_association(field)
127
+ end
55
128
 
56
- column = record.class.columns.find { |model_column| model_column.name.to_s == field.to_s }
57
- return default_field_type(field) if column.blank?
129
+ def relation_text_search_field(field)
130
+ [field, :text_search_field,
131
+ TextSearch.text_search_options(
132
+ model: relation(field).klass,
133
+ with: searchable_fields(field),
134
+ providing: relation(field).klass.primary_key
135
+ ).merge({ display_value: association_mapping(field).display_value, label: i18n.label(field) })]
136
+ end
137
+
138
+ def searchable_fields(field)
139
+ (Util.relation_controller(model, controller, field)&.active_element&.state&.searchable_fields || [])
140
+ .reject { |field| field.to_s.end_with?('_at') } # FIXME: Select strings/numbers only.
141
+ end
58
142
 
59
- default_type_from_column_type(field, column.type)
143
+ def relation_primary_key(field)
144
+ relation(field).options.fetch(:primary_key) { relation_model.primary_key }
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,48 @@ 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
+ {
208
+ required: required?(field)
209
+ }.merge(field_options(field))
210
+ end
211
+
212
+ def required?(field)
213
+ return false if record.blank?
214
+ return false unless record.class.respond_to?(:validators)
215
+
216
+ record.class.validators.find do |validator|
217
+ validator.kind == :presence && validator.attributes.include?(field.to_sym)
218
+ end
219
+ end
220
+
221
+ def field_options(field)
222
+ return NumericField.new(field: field, column: column(field)).options if numeric?(field)
223
+
224
+ {}
225
+ end
226
+
227
+ def numeric?(field)
228
+ %i[float decimal integer].include?(column(field)&.type)
229
+ end
101
230
  end
102
231
  end
103
232
  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('_', '-')&.tr('/', '-')
10
+ base = val&.to_s&.underscore&.tr('/', '_')
11
11
  plural ? base&.pluralize : base
12
12
  end
13
13
 
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ module Components
5
+ module Util
6
+ # Provides options for a `<input type="number" />` element based on database column properties.
7
+ class NumericField
8
+ def initialize(field:, column:)
9
+ @field = field
10
+ @column = column
11
+ end
12
+
13
+ def options
14
+ {
15
+ step: step,
16
+ min: min,
17
+ max: max
18
+ }.compact
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :field, :column
24
+
25
+ def step
26
+ return 'any' if column.blank?
27
+ return '1' if column.type == :integer
28
+ return 'any' if column.try(:scale).blank?
29
+
30
+ "0.#{'1'.rjust(column.scale, '0')}"
31
+ end
32
+
33
+ def min
34
+ return min_decimal if column.try(:precision).present?
35
+ return min_integer if column.try(:limit).present?
36
+
37
+ nil
38
+ end
39
+
40
+ def max
41
+ return max_decimal if column.try(:precision).present?
42
+ return max_integer if column.try(:limit).present?
43
+
44
+ nil
45
+ end
46
+
47
+ # XXX: This is the theoretical maximum value for a column with a given precision but,
48
+ # since the maximum database value is constrained by total significant figures (i.e.
49
+ # before and after the decimal point), an input can still be provided that would cause an
50
+ # error, so the default controller rescues `ActiveRecord::RangeError` to deal with this.
51
+ def max_decimal
52
+ '9' * column.precision
53
+ end
54
+
55
+ def min_decimal
56
+ "-#{'9' * column.precision}"
57
+ end
58
+
59
+ # `limit` represents available bytes for storing a signed integer e.g.
60
+ # 2**(8 * 8) / 2 - 1 == 9223372036854775807
61
+ # which matches PostgreSQL's `bigint` max value:
62
+ # https://www.postgresql.org/docs/current/datatype-numeric.html
63
+ def max_integer
64
+ ((2**(column.limit * 8)) / 2) - 1
65
+ end
66
+
67
+ def min_integer
68
+ -2**(column.limit * 8) / 2
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -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 value_from_record.class.is_a?(ActiveModel::Naming)
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 mapped_association_from_record
60
- AssociationMapping.new(
61
- component: component,
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
- ).link_tag
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
- alias integer_value numeric_value
75
- alias decimal_value numeric_value
76
- alias float_value numeric_value
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
- alias jsonb_value json_value
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
- alias text_value string_value
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
- alias binary_value boolean_value
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
@@ -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, record)
43
+ controller.helpers.public_send(default_record_path, *path_arguments)
40
44
  rescue NoMethodError
41
- controller.helpers.public_send(sti_record_path, record)
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, :update
83
+ when :edit
67
84
  'edit_'
68
- when :new, :create
85
+ when :new
69
86
  'new_'
70
87
  end
71
88
  end
@@ -9,6 +9,7 @@ require_relative 'util/form_value_mapping'
9
9
  require_relative 'util/display_value_mapping'
10
10
  require_relative 'util/association_mapping'
11
11
  require_relative 'util/decorator'
12
+ require_relative 'util/numeric_field'
12
13
 
13
14
  module ActiveElement
14
15
  module Components
@@ -29,18 +30,25 @@ module ActiveElement
29
30
  end
30
31
 
31
32
  def self.default_record_name(record)
32
- record.class.name.demodulize.underscore
33
+ (record.is_a?(Class) ? record.name : record.class.name).demodulize.underscore
34
+ end
35
+
36
+ def self.relation_controller(model, controller, relation)
37
+ namespace = controller.controller_path.rpartition('/').first.presence
38
+ base = "#{model.reflect_on_association(relation).klass.name.pluralize}Controller"
39
+ return base.safe_constantize if namespace.nil?
40
+
41
+ "#{namespace.classify}::#{base}".safe_constantize || base.safe_constantize
33
42
  end
34
43
 
35
44
  def self.json_pretty_print(json)
36
- theme = Rouge::Themes::Base16.mode(:light)
37
- formatter = Rouge::Formatters::HTMLLinewise.new(Rouge::Formatters::HTMLInline.new(theme))
45
+ formatter = Rouge::Formatters::HTML.new
38
46
  lexer = Rouge::Lexers::JSON.new
39
47
  content = JSON.pretty_generate(json.is_a?(String) ? JSON.parse(json) : json)
40
- formatted = formatter.format(lexer.lex(content)).gsub(' ', '&nbsp;&nbsp;')
48
+ formatted = formatter.format(lexer.lex(content)).gsub(' ', '&nbsp;&nbsp;').gsub("\n", '<br/>')
41
49
  # rubocop:disable Rails/OutputSafety
42
50
  # TODO: Move to a template.
43
- "<div style='font-family: monospace;'>#{formatted}</div>".html_safe
51
+ "<div class='json-highlight' style='font-family: monospace;'>#{formatted}</div>".html_safe
44
52
  # rubocop:enable Rails/OutputSafety
45
53
  end
46
54
  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
- (rails_component.environment == 'test' ? :yellow : :red)
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
 
@@ -16,7 +16,7 @@ module ActiveElement
16
16
  def initialize(controller_class, controller_instance = nil)
17
17
  @controller_class = controller_class
18
18
  @controller_instance = controller_instance
19
- initialize_state
19
+ initialize_state(controller_class)
20
20
  @missing_template_store = {}
21
21
  @authorize = false
22
22
  end
@@ -25,22 +25,64 @@ module ActiveElement
25
25
  @authorize
26
26
  end
27
27
 
28
+ def listable_fields(*args)
29
+ state.listable_fields.concat(args.map(&:to_sym)).uniq!
30
+ end
31
+
32
+ def viewable_fields(*args)
33
+ state.viewable_fields.concat(args.map(&:to_sym)).uniq!
34
+ end
35
+
36
+ def editable_fields(*args)
37
+ state.editable_fields.concat(args.map(&:to_sym)).uniq!
38
+ end
39
+
40
+ def searchable_fields(*args)
41
+ state.searchable_fields.concat(args.map(&:to_sym)).uniq!
42
+ end
43
+
44
+ def deletable
45
+ state.deletable = true
46
+ end
47
+
28
48
  def application_name
29
49
  RailsComponent.new(::Rails).application_name
30
50
  end
31
51
 
32
52
  def authenticate_with(&block)
33
- state[:authenticator] = block
53
+ state.authenticator = block
34
54
  end
35
55
 
36
56
  def authorize_with(&block)
37
57
  @authorize = true
38
- state[:authorizor] = block
58
+ state.authorizor = block
59
+ end
60
+
61
+ def sign_out_with(method: :get, &block)
62
+ state.sign_out_method = method
63
+ state.sign_out_path = block
64
+ end
65
+
66
+ def sign_out_path
67
+ state.sign_out_path&.call
68
+ end
69
+
70
+ delegate :sign_out_method, to: :state
71
+
72
+ def sign_in_with(method: :get, &block)
73
+ state.sign_in_method = method
74
+ state.sign_in_path = block
75
+ end
76
+
77
+ def sign_in_path
78
+ state.sign_in_path&.call
39
79
  end
40
80
 
81
+ delegate :sign_in_method, to: :state
82
+
41
83
  def authenticate
42
84
  authenticator&.call
43
- @current_user = state[:authorizor]&.call
85
+ @current_user = state.authorizor&.call
44
86
 
45
87
  nil
46
88
  end
@@ -49,16 +91,12 @@ module ActiveElement
49
91
  raise ArgumentError, "Must specify `with: '<permission>'` or `always: true`" unless with.present? || always
50
92
  raise ArgumentError, 'Cannot specify both `with` and `always: true`' if with.present? && always
51
93
 
52
- state[:permissions] << { with: with, always: always, action: action }
94
+ state.permissions << { with: with, always: always, action: action }
53
95
  end
54
96
 
55
- def authenticator
56
- state[:authenticator]
57
- end
97
+ delegate :authenticator, to: :state
58
98
 
59
- def permissions
60
- state.fetch(:permissions)
61
- end
99
+ delegate :permissions, to: :state
62
100
 
63
101
  def component
64
102
  return (@component ||= ActiveElement::Component.new(controller_instance)) unless controller_instance.nil?
@@ -66,16 +104,16 @@ module ActiveElement
66
104
  raise ArgumentError, 'Attempted to use ActiveElement component from a controller class method.'
67
105
  end
68
106
 
107
+ def state
108
+ self.class.state[controller_class]
109
+ end
110
+
69
111
  private
70
112
 
71
113
  attr_reader :controller_class, :controller_instance
72
114
 
73
- def initialize_state
74
- self.class.state[controller_class] ||= { permissions: [], authenticator: nil }
75
- end
76
-
77
- def state
78
- self.class.state[controller_class]
115
+ def initialize_state(key)
116
+ self.class.state[key] ||= ControllerState.new(controller: controller_instance)
79
117
  end
80
118
  end
81
119
  end