active_element 0.0.19 → 0.0.20

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f38b4565ed14ef722b7acd88d3c75742d4f07bdd0ba1e83459d4ebeed6e2adf3
4
- data.tar.gz: cbc7e900bf6d78d571602c172e6dcb535a14d328b426477d32ad0197ff584f3b
3
+ metadata.gz: 667ecfa2f26f661d4c0f760529fdc4ae944e242fdefecba10d6cc5b194b1e822
4
+ data.tar.gz: 28f0326bb18b3bf7fb402a5b89e82c796621825779fdaa2a754817e5028fe8e0
5
5
  SHA512:
6
- metadata.gz: e9561a790971bfc4a71d4fc18269f3b6a5972e3bcc788f12d448987d0483014a733eb16e04bfc3206f6c45a296fc2a5720955ddc4e45ed93261a72954013a64f
7
- data.tar.gz: c0a6457abe28261de7f42e9b9e0802c04f6e40363cc92b72bce25736ddcd9dbbbf858a253ce5a0c083baa943ba72301110473cc2566303d5118fb5c66fb41df0
6
+ metadata.gz: 1d7a79658f6ec7d58e2aac0492fe952d7df2c6d4873f2a4ad9390735b3f5c868947bb9722c92ec4bb640bf2909c3ad064e3ecaf7fccd5c07992810d9ee3344ee
7
+ data.tar.gz: d48338e4f22be093bf6b3c6ef3ca2acce3150a3db3fde1b715408b4f8de3ce41f2c6a3074982b4426fc53de6f5609b5de443f8192c5b602008f37abe1cbf0621
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- active_element (0.0.19)
4
+ active_element (0.0.20)
5
5
  bootstrap (~> 5.3.0alpha3)
6
6
  kaminari (~> 1.2)
7
7
  paintbrush (~> 0.1.2)
@@ -83,7 +83,7 @@ GEM
83
83
  autoprefixer-rails (10.4.16.0)
84
84
  execjs (~> 2)
85
85
  bcrypt (3.1.18)
86
- bootstrap (5.3.2)
86
+ bootstrap (5.3.3)
87
87
  autoprefixer-rails (>= 9.1.0)
88
88
  popper_js (>= 2.11.8, < 3)
89
89
  brakeman (5.4.1)
@@ -150,7 +150,7 @@ GEM
150
150
  mini_mime (1.1.5)
151
151
  mini_portile2 (2.8.2)
152
152
  minitest (5.18.1)
153
- net-imap (0.4.10)
153
+ net-imap (0.4.11)
154
154
  date
155
155
  net-protocol
156
156
  net-pop (0.1.2)
@@ -159,7 +159,7 @@ GEM
159
159
  timeout
160
160
  net-smtp (0.5.0)
161
161
  net-protocol
162
- nio4r (2.7.1)
162
+ nio4r (2.7.3)
163
163
  nokogiri (1.15.2)
164
164
  mini_portile2 (~> 2.8.2)
165
165
  racc (~> 1.4)
@@ -27,8 +27,8 @@ module ActiveElement
27
27
  helper_method :active_element
28
28
  helper_method :render_active_element_hook
29
29
 
30
- def render_active_element_hook(hook)
31
- render_to_string partial: hook
30
+ def render_active_element_hook(hook, locals: {})
31
+ render_to_string partial: hook, locals: locals
32
32
  rescue ActionView::MissingTemplate
33
33
  nil
34
34
  end
@@ -59,13 +59,23 @@
59
59
  <% fields.each_slice(columns) do |field_group| %>
60
60
  <div class="row form-fields mb-3">
61
61
  <% field_group.each do |field, type, options| %>
62
- <div class="col-sm-3">
63
- <%= render partial: 'active_element/components/form/label',
64
- locals: { component: component, id: id, type: type, form: form, field: field, options: options } %>
65
- </div>
62
+ <% if type != :hidden_field %>
63
+ <div class="col-sm-3">
64
+ <%= render partial: 'active_element/components/form/label',
65
+ locals: {
66
+ component: component,
67
+ id: id,
68
+ type: type,
69
+ form: form,
70
+ field: field,
71
+ options: options
72
+ } %>
73
+ </div>
74
+ <% end %>
66
75
 
67
76
 
68
- <div class="col">
77
+
78
+ <% if type != :hidden_field %><div class="col"><% end %>
69
79
  <%= render partial: 'active_element/components/form/field',
70
80
  locals: {
71
81
  id: id,
@@ -76,7 +86,7 @@
76
86
  component: component,
77
87
  record: record }
78
88
  %>
79
- </div>
89
+ <% if type != :hidden_field %></div><% end %>
80
90
  <% end %>
81
91
  </div>
82
92
  <% end %>
@@ -1,6 +1,6 @@
1
1
  <tr class="<%= (index % 2).zero? ? 'even' : 'odd' %> <%= row_class_mapper.call(item) %>">
2
2
  <% fields.each do |field, class_mapper, label, value_mapper| %>
3
- <td class="align-middle <%= class_mapper.call(item) %>">
3
+ <td class="align-top <%= class_mapper.call(item) %>">
4
4
  <% if component.secret_field?(field) %>
5
5
  <%= controller.helpers.render partial: 'active_element/components/secret/field',
6
6
  locals: { secret: value_mapper.call(item), label: label } %>
@@ -1,5 +1,10 @@
1
1
  <% if new %>
2
- <%= active_element.component.new_button(component.model&.new, float: 'end', class: 'mb-3') %>
2
+ <%= active_element.component.new_button(
3
+ component.model&.new,
4
+ nested_for: nested_for,
5
+ float: 'end',
6
+ class: 'mb-3'
7
+ ) %>
3
8
  <% end %>
4
9
 
5
10
 
@@ -1,9 +1,9 @@
1
1
  <%= active_element.component.page_title record.model_name.to_s.titleize %>
2
2
 
3
- <%= render_active_element_hook "#{controller_path}/before_edit" %>
3
+ <%= render_active_element_hook "#{controller_path}/before_edit", locals: { record: record } %>
4
4
 
5
5
  <%= active_element.component.form model: [namespace, record].compact,
6
6
  destroy: active_element.state.deletable?,
7
7
  fields: active_element.state.editable_fields %>
8
8
 
9
- <%= render_active_element_hook "#{controller_path}/after_edit" %>
9
+ <%= render_active_element_hook "#{controller_path}/after_edit", locals: { record: record } %>
@@ -7,7 +7,17 @@
7
7
  fields: active_element.state.searchable_fields %>
8
8
  <% end %>
9
9
 
10
- <%= render_active_element_hook "#{controller_path}/before_index" %>
10
+ <%= render_active_element_hook "#{controller_path}/before_index", locals: { collection: collection } %>
11
+
12
+ <% if nested_for.present? %>
13
+ <%=
14
+ active_element.component.page_section_title(
15
+ nested_for.map do |nested_for_record|
16
+ ActiveElement::Components::Util::DefaultDisplayValue.new(object: nested_for_record).value
17
+ end.join(', ')
18
+ )
19
+ %>
20
+ <% end %>
11
21
 
12
22
  <% if active_element.state.search_required && search_filters.compact_blank.blank? %>
13
23
  <% if active_element.state.creatable? %>
@@ -20,8 +30,9 @@
20
30
  show: active_element.state.viewable?,
21
31
  edit: active_element.state.editable?,
22
32
  destroy: active_element.state.deletable?,
33
+ nested_for: nested_for,
23
34
  collection: collection,
24
35
  fields: active_element.state.listable_fields %>
25
36
  <% end %>
26
37
 
27
- <%= render_active_element_hook "#{controller_path}/after_index" %>
38
+ <%= render_active_element_hook "#{controller_path}/after_index", locals: { collection: collection } %>
@@ -1,10 +1,10 @@
1
1
  <%= active_element.component.page_title record.model_name.to_s.titleize %>
2
2
 
3
- <%= render_active_element_hook "#{controller_path}/before_show" %>
3
+ <%= render_active_element_hook "#{controller_path}/before_show", locals: { record: record } %>
4
4
 
5
5
  <%= active_element.component.table item: record,
6
6
  edit: active_element.state.editable?,
7
7
  destroy: active_element.state.deletable?,
8
8
  fields: active_element.state.viewable_fields %>
9
9
 
10
- <%= render_active_element_hook "#{controller_path}/after_show" %>
10
+ <%= render_active_element_hook "#{controller_path}/after_show", locals: { record: record } %>
@@ -19,6 +19,15 @@
19
19
  </script>
20
20
  <% end %>
21
21
 
22
+ <% if respond_to?(:javascript_pack_tag) && defined? Webpacker %>
23
+ <%= begin
24
+ javascript_pack_tag 'application'
25
+ rescue Webpacker::Manifest::MissingEntryError
26
+ nil
27
+ end
28
+ %>
29
+ <% end %>
30
+
22
31
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
23
32
  <link rel="stylesheet"
24
33
  href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/default.min.css">
@@ -116,6 +125,5 @@
116
125
  <%= javascript_include_tag 'active_element/active_element', 'data-turbo-track': 'reload', 'data-turbolinks-track': 'reload' %>
117
126
  <%= javascript_include_tag 'application', 'data-turbo-track': 'reload', 'data-turbolinks-track': 'reload' %>
118
127
  <% end %>
119
-
120
128
  </body>
121
129
  </html>
data/config/routes.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  ActiveElement::Engine.routes.draw do
4
4
  ActiveElement.eager_load_controllers
5
+ ActiveElement.eager_load_models
5
6
 
6
7
  ActiveElement::ApplicationController.descendants.map do |descendant|
7
8
  post "#{descendant.controller_path}/_active_element_text_search",
@@ -6,7 +6,7 @@ module ActiveElement
6
6
  class Button
7
7
  # rubocop:disable Metrics/MethodLength
8
8
  def initialize(controller, record, flag_or_options, confirm: false, type: :primary, method: nil,
9
- float: nil, icon: nil, tooltip: false, **kwargs, &block)
9
+ float: nil, icon: nil, tooltip: false, nested_for: nil, **kwargs, &block)
10
10
  @controller = controller
11
11
  @record = record.is_a?(ActiveRecord::Relation) ? record.klass.new : record
12
12
  @flag_or_options = flag_or_options
@@ -20,6 +20,7 @@ module ActiveElement
20
20
  @block_given = block_given?
21
21
  @content = block.call if block_given?
22
22
  @tooltip = tooltip
23
+ @nested_for = nested_for
23
24
  end
24
25
  # rubocop:enable Metrics/MethodLength
25
26
 
@@ -49,7 +50,7 @@ module ActiveElement
49
50
  private
50
51
 
51
52
  attr_reader :controller, :record, :flag_or_options, :float, :kwargs, :kwargs_class, :type, :method, :icon,
52
- :block_given, :content, :confirm, :tooltip
53
+ :block_given, :content, :confirm, :tooltip, :nested_for
53
54
 
54
55
  def link_method
55
56
  return method if method.present?
@@ -116,7 +117,21 @@ module ActiveElement
116
117
  def record_path
117
118
  return nil unless record.class.is_a?(ActiveModel::Naming)
118
119
 
119
- Util::RecordPath.new(record: record, controller: controller, type: type).path
120
+ Util::RecordPath.new(record: record, controller: controller, type: type).path(**nested_args)
121
+ end
122
+
123
+ def nested_args
124
+ case type
125
+ when :new
126
+ nested_params
127
+ else
128
+ {}
129
+ end
130
+ end
131
+
132
+ def nested_params
133
+ route = controller.request.routes.recognize_path(controller.request.path)
134
+ route.reject { |key, _value| %w[controller action].include?(key.to_s) }
120
135
  end
121
136
  end
122
137
  end
@@ -15,7 +15,7 @@ module ActiveElement
15
15
  # rubocop:disable Metrics/MethodLength
16
16
  def initialize(controller, class_name:, collection:, fields:, params:, model_name: nil, style: nil,
17
17
  show: false, new: false, edit: false, destroy: false, paginate: true, group: nil,
18
- group_title: false, row_class: nil, title: nil, **_kwargs)
18
+ group_title: false, nested_for: nil, row_class: nil, title: nil, **_kwargs)
19
19
  @controller = controller
20
20
  @class_name = class_name
21
21
  @model_name = model_name
@@ -32,6 +32,7 @@ module ActiveElement
32
32
  @group_title = group_title
33
33
  @row_class = row_class
34
34
  @title = title
35
+ @nested_for = nested_for
35
36
  verify_paginate_and_group
36
37
  end
37
38
  # rubocop:enable Metrics/MethodLength
@@ -54,6 +55,7 @@ module ActiveElement
54
55
  destroy: destroy,
55
56
  group: group,
56
57
  group_title: group_title,
58
+ nested_for: nested_for,
57
59
  display_pagination: display_pagination?,
58
60
  page_sizes: [5, 10, 25, 50, 75, 100, 200],
59
61
  page_size: page_size,
@@ -78,7 +80,7 @@ module ActiveElement
78
80
 
79
81
  attr_reader :class_name, :collection, :fields, :style, :row_class,
80
82
  :new, :show, :edit, :destroy,
81
- :paginate, :params, :group, :group_title, :title
83
+ :paginate, :params, :group, :group_title, :title, :nested_for
82
84
 
83
85
  def paginated_collection
84
86
  return collection unless paginate && collection.respond_to?(:page) && !limit?
@@ -190,14 +190,30 @@ module ActiveElement
190
190
  end
191
191
 
192
192
  def base_options_for_select(field, field_options)
193
- return normalized_options(field_options.fetch(:options)) if field_options.key?(:options)
193
+ return normalized_options(field_options.fetch(:options), field_options) if field_options.key?(:options)
194
194
  return default_options_for_select(field, field_options) if record.class.is_a?(ActiveModel::Naming)
195
195
 
196
196
  raise ArgumentError, "Must provide select options `[:#{field}, { options: [...] }]` or a record instance."
197
197
  end
198
198
 
199
- def normalized_options(options)
200
- options.map { |option| option.is_a?(Array) ? option : [option, option] }
199
+ def normalized_options(options, field_options)
200
+ options.map do |option|
201
+ next option if option.is_a?(Array)
202
+ next active_record_option(option, field_options) if option.is_a?(ActiveRecord::Base)
203
+ [option, option] if option.is_a?(String)
204
+ end
205
+ end
206
+
207
+ def active_record_option(option, field_options)
208
+ [active_record_display_value(option, field_options), option.send(option.class.primary_key)]
209
+ end
210
+
211
+ def active_record_display_value(option, field_options)
212
+ if field_options[:display_value].is_a?(Proc) && record.present?
213
+ field_options[:display_value].call(option)
214
+ else
215
+ Util::DefaultDisplayValue.new(object: option).value
216
+ end
201
217
  end
202
218
 
203
219
  def default_class_name
@@ -12,6 +12,10 @@ module ActiveElement
12
12
  end
13
13
 
14
14
  def value
15
+ if object.respond_to?(:default_display_attribute)
16
+ return object.public_send(object.default_display_attribute)
17
+ end
18
+
15
19
  DEFAULT_FIELDS.each do |field|
16
20
  return object.public_send(field) if active_record_value?(field)
17
21
  return object[field] if hash_key(field) if hash_value?(field)
@@ -59,10 +59,12 @@ module ActiveElement
59
59
  end
60
60
 
61
61
  def inline_configured_field(field)
62
- field_options = FieldOptions.from_state(field, controller.active_element.state, record)
62
+ field_options = FieldOptions.from_state(
63
+ field, controller.active_element.state, record, controller
64
+ )
63
65
  return nil if field_options.blank?
64
66
 
65
- [field, field_options.type, field_options.options]
67
+ [field, field_options.type, field_options.options.reverse_merge({ value: field_options.value })]
66
68
  end
67
69
 
68
70
  def field_with_provided_type_and_provided_options(field)
@@ -201,6 +203,7 @@ module ActiveElement
201
203
  json: :json_field,
202
204
  jsonb: :json_field,
203
205
  geometry: :text_area,
206
+ text: :text_area,
204
207
  datetime: :datetime_field,
205
208
  date: :date_field,
206
209
  time: :time_field,
@@ -80,13 +80,22 @@ module ActiveElement
80
80
  end
81
81
 
82
82
  def value_from_config
83
- field_options = FieldOptions.from_state(field, component.controller.active_element.state, record)
83
+ field_options = field_options_from_state
84
84
  return nil if field_options.blank?
85
85
  return nil unless DATABASE_TYPES.include?(field_options.type.to_sym)
86
86
 
87
87
  send("#{field_options.type}_value")
88
88
  end
89
89
 
90
+ def field_options_from_state
91
+ FieldOptions.from_state(
92
+ field,
93
+ component.controller.active_element.state,
94
+ record,
95
+ component.controller
96
+ )
97
+ end
98
+
90
99
  # Override these methods as required in a class that includes this module:
91
100
 
92
101
  def mapped_association_from_record
@@ -25,8 +25,9 @@ module ActiveElement
25
25
  @authorize
26
26
  end
27
27
 
28
- def listable_fields(*args, order: nil)
28
+ def listable_fields(*args, order: nil, scope: nil)
29
29
  state.list_order = order
30
+ state.list_scope = scope
30
31
  state.listable_fields.concat(args.map(&:to_sym)).uniq!
31
32
  end
32
33
 
@@ -8,7 +8,7 @@ module ActiveElement
8
8
  attr_reader :permissions, :listable_fields, :viewable_fields, :editable_fields, :searchable_fields,
9
9
  :field_options
10
10
  attr_accessor :sign_in_path, :sign_in, :sign_in_method, :sign_out_path, :sign_out_method,
11
- :deletable, :authorizor, :authenticator, :list_order, :search_required, :model
11
+ :deletable, :authorizor, :authenticator, :list_order, :list_scope, :search_required, :model
12
12
 
13
13
  def initialize(controller:)
14
14
  @controller = controller
@@ -15,7 +15,8 @@ module ActiveElement
15
15
  controller.render 'active_element/default_views/index',
16
16
  locals: {
17
17
  collection: ordered(collection),
18
- search_filters: default_text_search.search_filters
18
+ search_filters: default_text_search.search_filters,
19
+ nested_for: nested_relations
19
20
  }
20
21
  end
21
22
 
@@ -124,9 +125,12 @@ module ActiveElement
124
125
  end
125
126
 
126
127
  def collection
127
- return model.all unless default_text_search.text_search?
128
+ return model.public_send(list_scope).where(nested_scope) unless default_text_search.text_search?
128
129
 
129
- model.left_outer_joins(default_text_search.search_relations).where(*default_text_search.text_search)
130
+ model.public_send(list_scope)
131
+ .left_outer_joins(default_text_search.search_relations)
132
+ .where(nested_scope)
133
+ .where(*default_text_search.text_search)
130
134
  end
131
135
 
132
136
  def render_range_error(error:, action:)
@@ -140,6 +144,36 @@ module ActiveElement
140
144
 
141
145
  I18n.t('active_element.unexpected_error')
142
146
  end
147
+
148
+ def list_scope
149
+ return :all if state.list_scope.blank?
150
+ return state.list_scope.call(request) if state.list_scope.is_a?(Proc)
151
+
152
+ state.list_scope
153
+ end
154
+
155
+ def nested_scope
156
+ nested_params.presence || noop
157
+ end
158
+
159
+ def noop
160
+ Arel::Nodes::True.new.eq(Arel::Nodes::True.new)
161
+ end
162
+
163
+ def nested_params
164
+ route = controller.request.routes.recognize_path(controller.request.path)
165
+ route.reject { |key, _value| %w[controller action].include?(key.to_s) }
166
+ end
167
+
168
+ def nested_relations
169
+ return [] if nested_params.blank?
170
+
171
+ nested_params.map do |key, value|
172
+ collection.model.reflections.values.find do |reflection|
173
+ reflection.foreign_key.to_s == key.to_s
174
+ end&.klass&.find(value)
175
+ end.compact
176
+ end
143
177
  end
144
178
  end
145
179
  end
@@ -25,6 +25,7 @@ module ActiveElement
25
25
  conditions = search_filters.to_h.map do |key, value|
26
26
  next relation_matches(key, value) if relation?(key)
27
27
  next datetime_between(key, value) if datetime?(key)
28
+ next join(key, value) if key.to_s.include?('.')
28
29
  next model.arel_table[key].matches("#{value}%") if string_like_column?(key)
29
30
 
30
31
  model.arel_table[key].eq(value)
@@ -36,7 +37,15 @@ module ActiveElement
36
37
  end
37
38
 
38
39
  def search_relations
39
- search_filters.to_h.keys.map { |key| relation?(key) ? key.to_sym : nil }.compact
40
+ relation_joins = search_filters.to_h.keys.map { |key| relation?(key) ? key.to_sym : nil }.compact
41
+ (relation_joins + shorthand_joins).uniq
42
+ end
43
+
44
+ def shorthand_joins
45
+ search_filters.to_h
46
+ .keys
47
+ .select { |key| key.to_s.include?('.') }
48
+ .map { |key| key.partition('.').first.to_sym }
40
49
  end
41
50
 
42
51
  private
@@ -66,6 +75,11 @@ module ActiveElement
66
75
  end.compact
67
76
  end
68
77
 
78
+ def join(key, value)
79
+ table, _, column = key.to_s.partition('.')
80
+ relation(table).klass.arel_table[column].eq(value)
81
+ end
82
+
69
83
  def noop
70
84
  Arel::Nodes::True.new.eq(Arel::Nodes::True.new)
71
85
  end
@@ -1,14 +1,14 @@
1
1
  module ActiveElement
2
2
  class FieldOptions
3
- attr_accessor :type, :options
3
+ attr_accessor :type, :options, :value
4
4
  attr_reader :field
5
5
 
6
- def self.from_state(field, state, record)
6
+ def self.from_state(field, state, record, controller)
7
7
  block = state.field_options[field]
8
8
  return nil if block.blank?
9
9
 
10
10
  field_options = new(field)
11
- block.call(field_options, record)
11
+ block.call(field_options, record, controller)
12
12
  field_options
13
13
  end
14
14
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveElement
4
- VERSION = '0.0.19'
4
+ VERSION = '0.0.20'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_element
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.19
4
+ version: 0.0.20
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bob Farrell
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-04-08 00:00:00.000000000 Z
11
+ date: 2024-05-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bootstrap