datagrid 1.8.2 → 2.0.0

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -0
  3. data/CHANGELOG.md +11 -1
  4. data/{Readme.markdown → README.md} +44 -29
  5. data/app/assets/stylesheets/datagrid.css +145 -0
  6. data/app/views/datagrid/_enum_checkboxes.html.erb +5 -3
  7. data/app/views/datagrid/_form.html.erb +4 -4
  8. data/app/views/datagrid/_head.html.erb +26 -3
  9. data/app/views/datagrid/_range_filter.html.erb +5 -3
  10. data/app/views/datagrid/_row.html.erb +12 -1
  11. data/app/views/datagrid/_table.html.erb +4 -4
  12. data/datagrid.gemspec +8 -7
  13. data/lib/datagrid/active_model.rb +9 -17
  14. data/lib/datagrid/base.rb +39 -0
  15. data/lib/datagrid/column_names_attribute.rb +9 -11
  16. data/lib/datagrid/columns/column.rb +155 -133
  17. data/lib/datagrid/columns.rb +325 -115
  18. data/lib/datagrid/configuration.rb +33 -4
  19. data/lib/datagrid/core.rb +89 -54
  20. data/lib/datagrid/deprecated_object.rb +20 -0
  21. data/lib/datagrid/drivers/abstract_driver.rb +12 -23
  22. data/lib/datagrid/drivers/active_record.rb +24 -26
  23. data/lib/datagrid/drivers/array.rb +22 -14
  24. data/lib/datagrid/drivers/mongo_mapper.rb +15 -14
  25. data/lib/datagrid/drivers/mongoid.rb +15 -17
  26. data/lib/datagrid/drivers/sequel.rb +14 -19
  27. data/lib/datagrid/drivers.rb +2 -1
  28. data/lib/datagrid/engine.rb +11 -3
  29. data/lib/datagrid/filters/base_filter.rb +166 -143
  30. data/lib/datagrid/filters/boolean_filter.rb +19 -5
  31. data/lib/datagrid/filters/date_filter.rb +33 -35
  32. data/lib/datagrid/filters/date_time_filter.rb +24 -16
  33. data/lib/datagrid/filters/default_filter.rb +9 -3
  34. data/lib/datagrid/filters/dynamic_filter.rb +151 -105
  35. data/lib/datagrid/filters/enum_filter.rb +43 -19
  36. data/lib/datagrid/filters/extended_boolean_filter.rb +39 -31
  37. data/lib/datagrid/filters/float_filter.rb +15 -5
  38. data/lib/datagrid/filters/integer_filter.rb +21 -10
  39. data/lib/datagrid/filters/ranged_filter.rb +66 -45
  40. data/lib/datagrid/filters/select_options.rb +58 -49
  41. data/lib/datagrid/filters/string_filter.rb +9 -4
  42. data/lib/datagrid/filters.rb +204 -79
  43. data/lib/datagrid/form_builder.rb +116 -128
  44. data/lib/datagrid/generators/scaffold.rb +184 -0
  45. data/lib/datagrid/generators/views.rb +20 -0
  46. data/lib/datagrid/helper.rb +436 -69
  47. data/lib/datagrid/ordering.rb +26 -29
  48. data/lib/datagrid/rspec.rb +6 -10
  49. data/lib/datagrid/utils.rb +37 -30
  50. data/lib/datagrid/version.rb +3 -1
  51. data/lib/datagrid.rb +8 -28
  52. data/templates/base.rb.erb +6 -4
  53. data/templates/grid.rb.erb +1 -1
  54. metadata +17 -17
  55. data/app/assets/stylesheets/datagrid.sass +0 -134
  56. data/lib/datagrid/filters/composite_filters.rb +0 -49
  57. data/lib/datagrid/renderer.rb +0 -157
  58. data/lib/datagrid/scaffold.rb +0 -129
  59. data/lib/tasks/datagrid_tasks.rake +0 -15
  60. data/templates/controller.rb.erb +0 -6
  61. data/templates/index.html.erb +0 -5
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "action_view"
4
+ require "datagrid/deprecated_object"
2
5
 
3
6
  module Datagrid
4
7
  module FormBuilder
@@ -8,88 +11,112 @@ module Datagrid
8
11
  # * <tt>select</tt> for enum, xboolean filter types
9
12
  # * <tt>check_box</tt> for boolean filter type
10
13
  # * <tt>text_field</tt> for other filter types
11
- def datagrid_filter(filter_or_attribute, partials: nil, **options, &block)
14
+ def datagrid_filter(filter_or_attribute, **options, &block)
12
15
  filter = datagrid_get_filter(filter_or_attribute)
13
- options = add_html_classes({**filter.input_options, **options}, filter.name, datagrid_filter_html_class(filter))
14
- self.send( filter.form_builder_helper_name, filter, **options, &block)
16
+ if filter.range?
17
+ datagrid_range_filter(filter, options, &block)
18
+ elsif filter.enum_checkboxes?
19
+ datagrid_enum_checkboxes_filter(filter, options, &block)
20
+ elsif filter.type == :dynamic
21
+ datagrid_dynamic_filter(filter, options, &block)
22
+ else
23
+ datagrid_filter_input(filter, **options, &block)
24
+ end
15
25
  end
16
26
 
17
27
  # @param filter_or_attribute [Datagrid::Filters::BaseFilter, String, Symbol] filter object or filter name
18
28
  # @param text [String, nil] label text, defaults to <tt>filter.header</tt>
19
29
  # @param options [Hash] options of rails <tt>label</tt> helper
20
- # @return [String] a form label html for the corresponding filter name
30
+ # @return [String] a form label tag for the corresponding filter name
21
31
  def datagrid_label(filter_or_attribute, text = nil, **options, &block)
22
32
  filter = datagrid_get_filter(filter_or_attribute)
23
- label(filter.name, text || filter.header, **filter.label_options, **options, &block)
24
- end
25
-
26
- def datagrid_filter_input(attribute_or_filter, **options)
27
- filter = datagrid_get_filter(attribute_or_filter)
28
- value = object.filter_value_as_string(filter)
29
- if options[:type]&.to_sym == :textarea
30
- text_area filter.name, value: value, **options, type: nil
31
- else
32
- text_field filter.name, value: value, **options
33
- end
34
- end
35
-
36
- protected
37
- def datagrid_extended_boolean_filter(attribute_or_filter, options = {})
38
- datagrid_enum_filter(attribute_or_filter, options)
39
- end
40
-
41
- def datagrid_boolean_filter(attribute_or_filter, options = {})
42
- check_box(datagrid_get_attribute(attribute_or_filter), options)
33
+ options = { **filter.label_options, **options }
34
+ label(filter.name, text || filter.header, **options, &block)
43
35
  end
44
36
 
45
- def datagrid_date_filter(attribute_or_filter, options = {})
46
- datagrid_range_filter(:date, attribute_or_filter, options)
47
- end
48
-
49
- def datagrid_date_time_filter(attribute_or_filter, options = {})
50
- datagrid_range_filter(:datetime, attribute_or_filter, options)
51
- end
52
-
53
- def datagrid_default_filter(attribute_or_filter, options = {})
54
- datagrid_filter_input(attribute_or_filter, **options)
55
- end
56
-
57
- def datagrid_enum_filter(attribute_or_filter, options = {}, &block)
37
+ # @param [Datagrid::Filters::BaseFilter, String, Symbol] attribute_or_filter filter object or filter name
38
+ # @param options [Hash{Symbol => Object}] HTML attributes to assign to input tag
39
+ # * `type` - special attribute the determines an input tag to be made.
40
+ # Examples: `text`, `select`, `textarea`, `number`, `date` etc.
41
+ # @return [String] an input tag for the corresponding filter name
42
+ def datagrid_filter_input(attribute_or_filter, **options, &block)
58
43
  filter = datagrid_get_filter(attribute_or_filter)
59
- if filter.checkboxes?
60
- options = add_html_classes(options, 'checkboxes')
61
- elements = object.select_options(filter).map do |element|
62
- text, value = @template.send(:option_text_and_value, element)
63
- checked = enum_checkbox_checked?(filter, value)
64
- [value, text, checked]
44
+ options = add_filter_options(filter, **options)
45
+ type = options.delete(:type)&.to_sym
46
+ if %i[datetime-local date].include?(type)
47
+ if options.key?(:value) && options[:value].nil?
48
+ # https://github.com/rails/rails/pull/53387
49
+ options[:value] = ""
65
50
  end
66
- render_partial(
67
- 'enum_checkboxes',
68
- {
69
- elements: elements,
70
- form: self,
71
- filter: filter,
72
- options: options,
73
- }
74
- )
75
- else
51
+ elsif options[:value]
52
+ options[:value] = filter.format(options[:value])
53
+ end
54
+ case type
55
+ when :"datetime-local"
56
+ datetime_local_field filter.name, **options, &block
57
+ when :date
58
+ date_field filter.name, **options, &block
59
+ when :textarea
60
+ text_area filter.name, value: object.filter_value_as_string(filter), **options, &block
61
+ when :checkbox
62
+ value = options.fetch(:value, 1).to_s
63
+ options = { checked: true, **options } if filter.enum_checkboxes? && enum_checkbox_checked?(filter, value)
64
+ check_box filter.name, options, value
65
+ when :hidden
66
+ hidden_field filter.name, **options
67
+ when :number
68
+ number_field filter.name, **options
69
+ when :select
76
70
  select(
77
71
  filter.name,
78
72
  object.select_options(filter) || [],
79
73
  {
80
74
  include_blank: filter.include_blank,
81
75
  prompt: filter.prompt,
82
- include_hidden: false
76
+ include_hidden: false,
83
77
  },
84
- multiple: filter.multiple?,
85
- **options,
86
- &block
78
+ multiple: filter.multiple?,
79
+ **options,
80
+ &block
87
81
  )
82
+ else
83
+ text_field filter.name, value: object.filter_value_as_string(filter), **options, &block
84
+ end
85
+ end
86
+
87
+ protected
88
+
89
+ def datagrid_enum_checkboxes_filter(filter, options = {})
90
+ elements = object.select_options(filter).map do |element|
91
+ text, value = @template.send(:option_text_and_value, element)
92
+ checked = enum_checkbox_checked?(filter, value)
93
+ [value, text, checked]
88
94
  end
95
+ choices = elements.map do |value, text, *_|
96
+ [value, text]
97
+ end
98
+ render_partial(
99
+ "enum_checkboxes",
100
+ {
101
+ form: self,
102
+ elements: Datagrid::DeprecatedObject.new(
103
+ elements,
104
+ ) do
105
+ Datagrid::Utils.warn_once(
106
+ <<~MSG,
107
+ Using `elements` variable in enum_checkboxes view is deprecated, use `choices` instead.
108
+ MSG
109
+ )
110
+ end,
111
+ choices: choices,
112
+ filter: filter,
113
+ options: options,
114
+ },
115
+ )
89
116
  end
90
117
 
91
118
  def enum_checkbox_checked?(filter, option_value)
92
- current_value = object.public_send(filter.name)
119
+ current_value = object.filter_value(filter)
93
120
  if current_value.respond_to?(:include?)
94
121
  # Typecast everything to string
95
122
  # to remove difference between String and Symbol
@@ -99,19 +126,9 @@ module Datagrid
99
126
  end
100
127
  end
101
128
 
102
- def datagrid_integer_filter(attribute_or_filter, options = {})
103
- filter = datagrid_get_filter(attribute_or_filter)
104
- if filter.multiple? && object[filter.name].blank?
105
- options[:value] = ""
106
- end
107
- datagrid_range_filter(:integer, filter, options)
108
- end
109
-
110
- def datagrid_dynamic_filter(attribute_or_filter, options = {})
111
- filter = datagrid_get_filter(attribute_or_filter)
112
- input_name = "#{object_name}[#{filter.name.to_s}][]"
129
+ def datagrid_dynamic_filter(filter, options = {})
113
130
  field, operation, value = object.filter_value(filter)
114
- options = options.merge(name: input_name)
131
+ options = add_filter_options(filter, **options)
115
132
  field_input = dynamic_filter_select(
116
133
  filter.name,
117
134
  object.select_options(filter) || [],
@@ -119,9 +136,10 @@ module Datagrid
119
136
  include_blank: filter.include_blank,
120
137
  prompt: filter.prompt,
121
138
  include_hidden: false,
122
- selected: field
139
+ selected: field,
123
140
  },
124
- add_html_classes(options, "field")
141
+ **add_html_classes(options, "datagrid-dynamic-field"),
142
+ name: @template.field_name(object_name, filter.name, "field"),
125
143
  )
126
144
  operation_input = dynamic_filter_select(
127
145
  filter.name, filter.operations_select,
@@ -131,9 +149,15 @@ module Datagrid
131
149
  prompt: false,
132
150
  selected: operation,
133
151
  },
134
- add_html_classes(options, "operation")
152
+ **add_html_classes(options, "datagrid-dynamic-operation"),
153
+ name: @template.field_name(object_name, filter.name, "operation"),
154
+ )
155
+ value_input = datagrid_filter_input(
156
+ filter.name,
157
+ **add_html_classes(options, "datagrid-dynamic-value"),
158
+ value: value,
159
+ name: @template.field_name(object_name, filter.name, "value"),
135
160
  )
136
- value_input = text_field(filter.name, **add_html_classes(options, "value"), value: value)
137
161
  [field_input, operation_input, value_input].join("\n").html_safe
138
162
  end
139
163
 
@@ -149,61 +173,26 @@ module Datagrid
149
173
  end
150
174
  end
151
175
 
152
- def datagrid_range_filter(type, attribute_or_filter, options = {})
153
- filter = datagrid_get_filter(attribute_or_filter)
154
- if filter.range?
155
- options = options.merge(multiple: true)
156
- from_options = datagrid_range_filter_options(object, filter, :from, options)
157
- to_options = datagrid_range_filter_options(object, filter, :to, options)
158
- render_partial 'range_filter', {
159
- from_options: from_options, to_options: to_options, filter: filter, form: self
160
- }
161
- else
162
- datagrid_filter_input(filter, **options)
163
- end
176
+ def datagrid_range_filter(filter, options = {})
177
+ from_options = datagrid_range_filter_options(object, filter, :from, **options)
178
+ to_options = datagrid_range_filter_options(object, filter, :to, **options)
179
+ render_partial "range_filter", {
180
+ from_options: from_options, to_options: to_options, filter: filter, form: self,
181
+ }
164
182
  end
165
183
 
166
- def datagrid_range_filter_options(object, filter, type, options)
167
- type_method_map = {from: :first, to: :last}
168
- options = add_html_classes(options, type)
169
- options[:value] = filter.format(object[filter.name]&.public_send(type_method_map[type]))
170
- # In case of datagrid ranged filter
171
- # from and to input will have same id
172
- if !options.key?(:id)
173
- # Rails provides it's own default id for all inputs
174
- # In order to prevent that we assign no id by default
175
- options[:id] = nil
176
- elsif options[:id].present?
177
- # If the id was given we prefix it
178
- # with from_ and to_ accordingly
179
- options[:id] = [type, options[:id]].join("_")
180
- end
184
+ def datagrid_range_filter_options(object, filter, section, **options)
185
+ type_method_map = { from: :begin, to: :end }
186
+ options[:value] = object[filter.name]&.public_send(type_method_map[section])
187
+ options[:name] = @template.field_name(object_name, filter.name, section)
181
188
  options
182
189
  end
183
190
 
184
- def datagrid_string_filter(attribute_or_filter, options = {})
185
- datagrid_range_filter(:string, attribute_or_filter, options)
186
- end
187
-
188
- def datagrid_float_filter(attribute_or_filter, options = {})
189
- datagrid_range_filter(:float, attribute_or_filter, options)
190
- end
191
-
192
- def datagrid_get_attribute(attribute_or_filter)
193
- Utils.string_like?(attribute_or_filter) ? attribute_or_filter : attribute_or_filter.name
194
- end
195
-
196
191
  def datagrid_get_filter(attribute_or_filter)
197
- if Utils.string_like?(attribute_or_filter)
198
- object.class.filter_by_name(attribute_or_filter) ||
199
- raise(Error, "Datagrid filter #{attribute_or_filter} not found")
200
- else
201
- attribute_or_filter
202
- end
203
- end
192
+ return attribute_or_filter unless Utils.string_like?(attribute_or_filter)
204
193
 
205
- def datagrid_filter_html_class(filter)
206
- filter.class.to_s.demodulize.underscore
194
+ object.class.filter_by_name(attribute_or_filter) ||
195
+ raise(ArgumentError, "Datagrid filter #{attribute_or_filter} not found")
207
196
  end
208
197
 
209
198
  def add_html_classes(options, *classes)
@@ -211,21 +200,20 @@ module Datagrid
211
200
  end
212
201
 
213
202
  def partial_path(name)
214
- if partials = self.options[:partials]
203
+ if (partials = options[:partials])
215
204
  partial_name = File.join(partials, name)
216
205
  # Second argument is []: no magical namespaces to lookup added from controller
217
- if @template.lookup_context.template_exists?(partial_name, [], true)
218
- return partial_name
219
- end
206
+ return partial_name if @template.lookup_context.template_exists?(partial_name, [], true)
220
207
  end
221
- File.join('datagrid', name)
208
+ File.join("datagrid", name)
222
209
  end
223
210
 
224
211
  def render_partial(name, locals)
225
212
  @template.render partial: partial_path(name), locals: locals
226
213
  end
227
214
 
228
- class Error < StandardError
215
+ def add_filter_options(filter, **options)
216
+ { **filter.default_input_options, **filter.input_options, **options }
229
217
  end
230
218
  end
231
219
  end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Datagrid
6
+ # @!visibility private
7
+ module Generators
8
+ # @!visibility private
9
+ class Scaffold < Rails::Generators::NamedBase
10
+ include Rails::Generators::ResourceHelpers
11
+
12
+ check_class_collision suffix: "Grid"
13
+ source_root File.expand_path("#{__FILE__}/../../../templates")
14
+
15
+ def create_scaffold
16
+ template "base.rb.erb", base_grid_file unless file_exists?(base_grid_file)
17
+ template "grid.rb.erb", "app/grids/#{grid_class_name.underscore}.rb"
18
+ if file_exists?(grid_controller_file)
19
+ inject_into_file grid_controller_file, index_action, after: %r{class .*#{grid_controller_class_name}.*\n}
20
+ else
21
+ create_file grid_controller_file, controller_code
22
+ end
23
+ create_file view_file, view_code
24
+ route(generate_routing_namespace("resources :#{grid_controller_short_name}"))
25
+ gem "kaminari" unless kaminari? || will_paginate? || pagy?
26
+ in_root do
27
+ {
28
+ "css" => " *= require datagrid",
29
+ "css.sass" => " *= require datagrid",
30
+ "css.scss" => " *= require datagrid",
31
+ }.each do |extension, string|
32
+ file = "app/assets/stylesheets/application.#{extension}"
33
+ if file_exists?(file)
34
+ inject_into_file file, "#{string}\n", { before: %r{.*require_self} } # before all
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ def view_file
41
+ Rails.root.join("app/views").join(controller_file_path).join("index.html.erb")
42
+ end
43
+
44
+ def grid_class_name
45
+ "#{file_name.camelize.pluralize}Grid"
46
+ end
47
+
48
+ def grid_base_class
49
+ file_exists?("app/grids/base_grid.rb") ? "BaseGrid" : "ApplicationGrid"
50
+ end
51
+
52
+ def grid_controller_class_name
53
+ "#{controller_class_name.camelize}Controller"
54
+ end
55
+
56
+ def grid_controller_file
57
+ Rails.root.join("app/controllers").join("#{grid_controller_class_name.underscore}.rb")
58
+ end
59
+
60
+ def grid_controller_short_name
61
+ controller_file_name
62
+ end
63
+
64
+ def grid_model_name
65
+ file_name.camelize.singularize
66
+ end
67
+
68
+ def grid_param_name
69
+ grid_class_name.underscore
70
+ end
71
+
72
+ def pagination_helper_code
73
+ if will_paginate?
74
+ "will_paginate(@grid.assets)"
75
+ elsif pagy?
76
+ "pagy_nav(@pagy)"
77
+ else
78
+ # Kaminari is default
79
+ "paginate(@grid.assets)"
80
+ end
81
+ end
82
+
83
+ def table_helper_code
84
+ if pagy?
85
+ "datagrid_table @grid, @records"
86
+ else
87
+ "datagrid_table @grid"
88
+ end
89
+ end
90
+
91
+ def base_grid_file
92
+ "app/grids/application_grid.rb"
93
+ end
94
+
95
+ def grid_route_name
96
+ "#{controller_class_name.underscore.gsub('/', '_')}_path"
97
+ end
98
+
99
+ def index_code
100
+ if pagy?
101
+ <<-RUBY
102
+ @grid = #{grid_class_name}.new(grid_params)
103
+ @pagy, @assets = pagy(@grid.assets)
104
+ RUBY
105
+ else
106
+ <<-RUBY
107
+ @grid = #{grid_class_name}.new(grid_params) do |scope|
108
+ scope.page(params[:page])
109
+ end
110
+ RUBY
111
+ end
112
+ end
113
+
114
+ def controller_code
115
+ <<~RUBY
116
+ class #{grid_controller_class_name} < ApplicationController
117
+ def index
118
+ #{index_code.rstrip}
119
+ end
120
+
121
+ protected
122
+
123
+ def grid_params
124
+ params.fetch(:#{grid_param_name}, {}).permit!
125
+ end
126
+ end
127
+ RUBY
128
+ end
129
+
130
+ def view_code
131
+ <<~ERB
132
+ <%= datagrid_form_with model: @grid, url: #{grid_route_name} %>
133
+
134
+ <%= #{pagination_helper_code} %>
135
+ <%= #{table_helper_code} %>
136
+ <%= #{pagination_helper_code} %>
137
+ ERB
138
+ end
139
+
140
+ protected
141
+
142
+ def generate_routing_namespace(code)
143
+ depth = regular_class_path.length
144
+ # Create 'namespace' ladder
145
+ # namespace :foo do
146
+ # namespace :bar do
147
+ namespace_ladder = regular_class_path.each_with_index.map do |ns, i|
148
+ indent("namespace :#{ns} do\n", i * 2)
149
+ end.join
150
+
151
+ # Create route
152
+ # get 'baz/index'
153
+ route = indent(code, depth * 2)
154
+
155
+ # Create `end` ladder
156
+ # end
157
+ # end
158
+ end_ladder = (1..depth).reverse_each.map do |i|
159
+ indent("end\n", i * 2)
160
+ end.join
161
+
162
+ # Combine the 3 parts to generate complete route entry
163
+ "#{namespace_ladder}#{route}\n#{end_ladder}"
164
+ end
165
+
166
+ def file_exists?(name)
167
+ name = Rails.root.join(name) unless name.to_s.first == "/"
168
+ File.exist?(name)
169
+ end
170
+
171
+ def pagy?
172
+ defined?(::Pagy)
173
+ end
174
+
175
+ def will_paginate?
176
+ defined?(::WillPaginate)
177
+ end
178
+
179
+ def kaminari?
180
+ defined?(::Kaminari)
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datagrid
4
+ module Generators
5
+ class Views < Rails::Generators::Base
6
+ source_root File.expand_path("../../../app/views/datagrid", __dir__)
7
+
8
+ desc "Copies Datagrid partials to your application."
9
+ def copy_views
10
+ Dir.glob(File.join(self.class.source_root, "**", "*")).each do |file_path|
11
+ relative_path = file_path.sub("#{self.class.source_root}/", "")
12
+
13
+ next if relative_path == "_order_for.html.erb"
14
+
15
+ copy_file(relative_path, File.join("app/views/datagrid", relative_path))
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end