datagrid 1.8.1 → 2.0.0.pre.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -7
  3. data/{Readme.markdown → README.md} +46 -29
  4. data/app/assets/stylesheets/datagrid.css +145 -0
  5. data/app/views/datagrid/_enum_checkboxes.html.erb +5 -3
  6. data/app/views/datagrid/_form.html.erb +4 -5
  7. data/app/views/datagrid/_head.html.erb +26 -3
  8. data/app/views/datagrid/_range_filter.html.erb +5 -3
  9. data/app/views/datagrid/_row.html.erb +12 -1
  10. data/app/views/datagrid/_table.html.erb +4 -4
  11. data/datagrid.gemspec +8 -8
  12. data/lib/datagrid/active_model.rb +9 -17
  13. data/lib/datagrid/base.rb +39 -0
  14. data/lib/datagrid/column_names_attribute.rb +12 -12
  15. data/lib/datagrid/columns/column.rb +155 -133
  16. data/lib/datagrid/columns.rb +495 -282
  17. data/lib/datagrid/configuration.rb +23 -10
  18. data/lib/datagrid/core.rb +184 -150
  19. data/lib/datagrid/deprecated_object.rb +20 -0
  20. data/lib/datagrid/drivers/abstract_driver.rb +13 -25
  21. data/lib/datagrid/drivers/active_record.rb +24 -26
  22. data/lib/datagrid/drivers/array.rb +26 -17
  23. data/lib/datagrid/drivers/mongo_mapper.rb +15 -14
  24. data/lib/datagrid/drivers/mongoid.rb +16 -18
  25. data/lib/datagrid/drivers/sequel.rb +14 -19
  26. data/lib/datagrid/drivers.rb +2 -1
  27. data/lib/datagrid/engine.rb +11 -3
  28. data/lib/datagrid/filters/base_filter.rb +166 -142
  29. data/lib/datagrid/filters/boolean_filter.rb +19 -5
  30. data/lib/datagrid/filters/date_filter.rb +33 -35
  31. data/lib/datagrid/filters/date_time_filter.rb +24 -16
  32. data/lib/datagrid/filters/default_filter.rb +9 -3
  33. data/lib/datagrid/filters/dynamic_filter.rb +151 -105
  34. data/lib/datagrid/filters/enum_filter.rb +43 -19
  35. data/lib/datagrid/filters/extended_boolean_filter.rb +39 -27
  36. data/lib/datagrid/filters/float_filter.rb +16 -5
  37. data/lib/datagrid/filters/integer_filter.rb +21 -10
  38. data/lib/datagrid/filters/ranged_filter.rb +66 -45
  39. data/lib/datagrid/filters/select_options.rb +58 -49
  40. data/lib/datagrid/filters/string_filter.rb +9 -4
  41. data/lib/datagrid/filters.rb +234 -106
  42. data/lib/datagrid/form_builder.rb +116 -128
  43. data/lib/datagrid/generators/scaffold.rb +185 -0
  44. data/lib/datagrid/generators/views.rb +20 -0
  45. data/lib/datagrid/helper.rb +397 -22
  46. data/lib/datagrid/ordering.rb +81 -87
  47. data/lib/datagrid/rspec.rb +8 -12
  48. data/lib/datagrid/utils.rb +42 -38
  49. data/lib/datagrid/version.rb +3 -1
  50. data/lib/datagrid.rb +18 -28
  51. data/templates/base.rb.erb +33 -7
  52. data/templates/grid.rb.erb +1 -1
  53. metadata +18 -19
  54. data/app/assets/stylesheets/datagrid.sass +0 -134
  55. data/lib/datagrid/filters/composite_filters.rb +0 -49
  56. data/lib/datagrid/renderer.rb +0 -157
  57. data/lib/datagrid/scaffold.rb +0 -129
  58. data/lib/tasks/datagrid_tasks.rake +0 -15
  59. data/templates/controller.rb.erb +0 -6
  60. 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.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].try(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,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ # @!visibility private
6
+ module Datagrid
7
+ # @!visibility private
8
+ module Generators
9
+ # @!visibility private
10
+ class Scaffold < Rails::Generators::NamedBase
11
+ include Rails::Generators::ResourceHelpers
12
+
13
+ check_class_collision suffix: "Grid"
14
+ source_root File.expand_path("#{__FILE__}/../../../templates")
15
+
16
+ def create_scaffold
17
+ template "base.rb.erb", base_grid_file unless file_exists?(base_grid_file)
18
+ template "grid.rb.erb", "app/grids/#{grid_class_name.underscore}.rb"
19
+ if file_exists?(grid_controller_file)
20
+ inject_into_file grid_controller_file, index_action, after: %r{class .*#{grid_controller_class_name}.*\n}
21
+ else
22
+ create_file grid_controller_file, controller_code
23
+ end
24
+ create_file view_file, view_code
25
+ route(generate_routing_namespace("resources :#{grid_controller_short_name}"))
26
+ gem "kaminari" unless kaminari? || will_paginate? || pagy?
27
+ in_root do
28
+ {
29
+ "css" => " *= require datagrid",
30
+ "css.sass" => " *= require datagrid",
31
+ "css.scss" => " *= require datagrid",
32
+ }.each do |extension, string|
33
+ file = "app/assets/stylesheets/application.#{extension}"
34
+ if file_exists?(file)
35
+ inject_into_file file, "#{string}\n", { before: %r{.*require_self} } # before all
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ def view_file
42
+ Rails.root.join("app/views").join(controller_file_path).join("index.html.erb")
43
+ end
44
+
45
+ def grid_class_name
46
+ "#{file_name.camelize.pluralize}Grid"
47
+ end
48
+
49
+ def grid_base_class
50
+ file_exists?("app/grids/base_grid.rb") ? "BaseGrid" : "ApplicationGrid"
51
+ end
52
+
53
+ def grid_controller_class_name
54
+ "#{controller_class_name.camelize}Controller"
55
+ end
56
+
57
+ def grid_controller_file
58
+ Rails.root.join("app/controllers").join("#{grid_controller_class_name.underscore}.rb")
59
+ end
60
+
61
+ def grid_controller_short_name
62
+ controller_file_name
63
+ end
64
+
65
+ def grid_model_name
66
+ file_name.camelize.singularize
67
+ end
68
+
69
+ def grid_param_name
70
+ grid_class_name.underscore
71
+ end
72
+
73
+ def pagination_helper_code
74
+ if will_paginate?
75
+ "will_paginate(@grid.assets)"
76
+ elsif pagy?
77
+ "pagy_nav(@pagy)"
78
+ else
79
+ # Kaminari is default
80
+ "paginate(@grid.assets)"
81
+ end
82
+ end
83
+
84
+ def table_helper_code
85
+ if pagy?
86
+ "datagrid_table @grid, @records"
87
+ else
88
+ "datagrid_table @grid"
89
+ end
90
+ end
91
+
92
+ def base_grid_file
93
+ "app/grids/application_grid.rb"
94
+ end
95
+
96
+ def grid_route_name
97
+ "#{controller_class_name.underscore.gsub('/', '_')}_path"
98
+ end
99
+
100
+ def index_code
101
+ if pagy?
102
+ <<-RUBY
103
+ @grid = #{grid_class_name}.new(grid_params)
104
+ @pagy, @assets = pagy(@grid.assets)
105
+ RUBY
106
+ else
107
+ <<-RUBY
108
+ @grid = #{grid_class_name}.new(grid_params) do |scope|
109
+ scope.page(params[:page])
110
+ end
111
+ RUBY
112
+ end
113
+ end
114
+
115
+ def controller_code
116
+ <<~RUBY
117
+ class #{grid_controller_class_name} < ApplicationController
118
+ def index
119
+ #{index_code.rstrip}
120
+ end
121
+
122
+ protected
123
+
124
+ def grid_params
125
+ params.fetch(:#{grid_param_name}, {}).permit!
126
+ end
127
+ end
128
+ RUBY
129
+ end
130
+
131
+ def view_code
132
+ <<~ERB
133
+ <%= datagrid_form_with model: @grid, url: #{grid_route_name} %>
134
+
135
+ <%= #{pagination_helper_code} %>
136
+ <%= #{table_helper_code} %>
137
+ <%= #{pagination_helper_code} %>
138
+ ERB
139
+ end
140
+
141
+ protected
142
+
143
+ def generate_routing_namespace(code)
144
+ depth = regular_class_path.length
145
+ # Create 'namespace' ladder
146
+ # namespace :foo do
147
+ # namespace :bar do
148
+ namespace_ladder = regular_class_path.each_with_index.map do |ns, i|
149
+ indent("namespace :#{ns} do\n", i * 2)
150
+ end.join
151
+
152
+ # Create route
153
+ # get 'baz/index'
154
+ route = indent(code, depth * 2)
155
+
156
+ # Create `end` ladder
157
+ # end
158
+ # end
159
+ end_ladder = (1..depth).reverse_each.map do |i|
160
+ indent("end\n", i * 2)
161
+ end.join
162
+
163
+ # Combine the 3 parts to generate complete route entry
164
+ "#{namespace_ladder}#{route}\n#{end_ladder}"
165
+ end
166
+
167
+ def file_exists?(name)
168
+ name = Rails.root.join(name) unless name.to_s.first == "/"
169
+ File.exist?(name)
170
+ end
171
+
172
+ def pagy?
173
+ defined?(::Pagy)
174
+ end
175
+
176
+ def will_paginate?
177
+ defined?(::WillPaginate)
178
+ end
179
+
180
+ def kaminari?
181
+ defined?(::Kaminari)
182
+ end
183
+ end
184
+ end
185
+ 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