active_element 0.0.10 → 0.0.12

Sign up to get free protection for your applications and to get access to all the features.
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