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,9 +1,245 @@
1
- require "datagrid/engine"
1
+ # frozen_string_literal: true
2
+
2
3
  require "action_view"
3
4
 
4
5
  module Datagrid
6
+ # # Datagrid Frontend Guide
7
+ #
8
+ # ## Description
9
+ #
10
+ # The easiest way to start with Datagrid frontend is by using the generator:
11
+ #
12
+ # ``` sh
13
+ # rails generate datagrid:scaffold users
14
+ # ```
15
+ #
16
+ # This command builds the controller, view, route, and adds
17
+ # [built-in CSS](https://github.com/bogdan/datagrid/blob/master/app/assets/stylesheets/datagrid.sass).
18
+ #
19
+ # Datagrid includes helpers and a form builder for easy frontend generation.
20
+ # If you need a fully-featured custom GUI, create your templates manually with the help of the {Datagrid::Columns} API.
21
+ #
22
+ # ## Controller and Routing
23
+ #
24
+ # Grids usually implement the `index` action of a Rails REST resource. Here's an example:
25
+ #
26
+ # resources :models, only: [:index]
27
+ #
28
+ # Use the `GET` method in the form, and the controller becomes straightforward:
29
+ #
30
+ # class ModelsController < ApplicationController
31
+ # def index
32
+ # @grid = ModelsGrid.new(params[:my_report]) do |scope|
33
+ # scope.page(params[:page]) # See pagination section
34
+ # end
35
+ # end
36
+ # end
37
+ #
38
+ # To apply additional scoping conditions, such as visibility based on the current user:
39
+ #
40
+ # ModelsGrid.new(params[:my_report]) do |scope|
41
+ # scope.where(owner_id: current_user.id).page(params[:page])
42
+ # end
43
+ #
44
+ # To pass an object to a grid instance, define it as an accessible attribute:
45
+ #
46
+ # class ModelsGrid
47
+ # attr_accessor :current_user
48
+ # end
49
+ #
50
+ # Then pass it when initializing the grid:
51
+ #
52
+ # ModelsGrid.new(params[:models_grid].merge(current_user: current_user))
53
+ #
54
+ # ## Form Builder
55
+ #
56
+ # ### Basic Method
57
+ #
58
+ # Use the built-in partial:
59
+ #
60
+ # = datagrid_form_with model: @grid, url: report_path, other_form_for_option: value
61
+ #
62
+ # {#datagrid_form_with} supports the same options as Rails `form_with`.
63
+ #
64
+ # ### Advanced Method
65
+ #
66
+ # You can use Rails built-in tools to create a form. Additionally, Datagrid provides helpers to generate input/select elements for filters:
67
+ #
68
+ # ``` haml
69
+ # - form_with model: UserGrid.new, method: :get, url: users_path do |f|
70
+ # %div
71
+ # = f.datagrid_label :name
72
+ # = f.datagrid_filter :name # => <input name="grid[name]" type="text"/>
73
+ # %div
74
+ # = f.datagrid_label :category_id
75
+ # = f.datagrid_filter :category_id # => <select name="grid[category_id]">....</select>
76
+ # ```
77
+ #
78
+ # For more flexibility, use Rails default helpers:
79
+ #
80
+ # %div
81
+ # = f.label :name
82
+ # = f.text_field :name
83
+ #
84
+ # See the localization section of {Datagrid::Filters}.
85
+ #
86
+ # ## Datagrid Table
87
+ #
88
+ # Use the helper to display a report:
89
+ #
90
+ # %div== Total #{@grid.assets.total}
91
+ # = datagrid_table(@report)
92
+ # = will_paginate @report.assets
93
+ #
94
+ # Options:
95
+ #
96
+ # - `:html` - Attributes for the `<table>` tag.
97
+ # - `:order` - Set to `false` to disable ordering controls (default: `true`).
98
+ # - `:columns` - Specify an array of column names to display.
99
+ #
100
+ # ## Pagination
101
+ #
102
+ # Datagrid is abstracted from pagination but integrates seamlessly with tools like Kaminari, WillPaginate, or Pagy:
103
+ #
104
+ # # Kaminari
105
+ # @grid = MyGrid.new(params[:grid]) do |scope|
106
+ # scope.page(params[:page]).per(10)
107
+ # end
108
+ #
109
+ # # WillPaginate
110
+ # @grid = MyGrid.new(params[:grid]) do |scope|
111
+ # scope.page(params[:page]).per_page(10)
112
+ # end
113
+ #
114
+ # # Pagy
115
+ # @grid = MyGrid.new(params[:grid])
116
+ # @pagy, @records = pagy(@grid.assets)
117
+ #
118
+ # Render the paginated collection:
119
+ #
120
+ # # WillPaginate or Kaminari
121
+ # <%= datagrid_table(@grid, options) %>
122
+ # # Pagy
123
+ # <%= datagrid_table(@grid, @records, options) %>
124
+ #
125
+ # ## CSV Export
126
+ #
127
+ # Add CSV support to your controller:
128
+ #
129
+ # class UsersController < ApplicationController
130
+ # def index
131
+ # @grid = UsersGrid.new(params[:users_grid])
132
+ # respond_to do |f|
133
+ # f.html { @grid.scope { |scope| scope.page(params[:page]) } }
134
+ # f.csv do
135
+ # send_data @grid.to_csv, type: "text/csv", disposition: 'inline', filename: "grid-#{Time.now.to_s}.csv"
136
+ # end
137
+ # end
138
+ # end
139
+ # end
140
+ #
141
+ # Add a button in your interface:
142
+ #
143
+ # link_to "Get CSV", url_for(format: 'csv', users_grid: params[:users_grid])
144
+ #
145
+ # ## AJAX
146
+ #
147
+ # Datagrid supports asynchronous data loading. Add this to your controller:
148
+ #
149
+ # if request.xhr?
150
+ # render json: {table: view_context.datagrid_table(@grid)}
151
+ # end
152
+ #
153
+ # Modify the form for AJAX:
154
+ #
155
+ # = datagrid_form_with model: @grid, html: {class: 'js-datagrid-form'}
156
+ # .js-datagrid-table
157
+ # = datagrid_table @grid
158
+ # .js-pagination
159
+ # = paginate @grid.assets
160
+ # :javascript
161
+ # $('.js-datagrid-form').submit(function(event) {
162
+ # event.preventDefault();
163
+ # $.get($(this).attr("action"), $(this).serialize(), function (data) {
164
+ # $('.js-datagrid-table').html(data.table);
165
+ # });
166
+ # });
167
+ #
168
+ # ## Modifying Built-In Partials
169
+ #
170
+ # To customize Datagrid views:
171
+ #
172
+ # rake datagrid:copy_partials
173
+ #
174
+ # This creates files in `app/views/datagrid/`, which you can modify to suit your needs:
175
+ #
176
+ # app/views/datagrid/
177
+ # ├── _enum_checkboxes.html.erb # datagrid_filter for filter(name, :enum, checkboxes: true)
178
+ # ├── _form.html.erb # datagrid_form_with
179
+ # ├── _head.html.erb # datagrid_header
180
+ # ├── _range_filter.html.erb # datagrid_filter for filter(name, type, range: true)
181
+ # ├── _row.html.erb # datagrid_rows/datagrid_rows
182
+ # └── _table.html.erb # datagrid_table
183
+ #
184
+ # ## Custom Options
185
+ #
186
+ # You can add custom options to Datagrid columns and filters and implement their support on the frontend.
187
+ # For example, you might want to add a `description` option to a column that appears as a tooltip on mouseover.
188
+ #
189
+ # column(
190
+ # :aov, header: 'AOV',
191
+ # description: 'Average order value: sum of orders subtotal divided by their count'
192
+ # ) do |category|
193
+ # category.orders.sum(:subtotal) / category.orders.count
194
+ # end
195
+ #
196
+ # The `:description` option is not built into Datagrid, but you can implement it
197
+ # by adding the following to partial `app/views/datagrid/_header.html.erb`:
198
+ #
199
+ # <% if column.options[:description] %>
200
+ # <a data-toggle="tooltip" title="<%= column.options[:description] %>">
201
+ # <i class="icon-question-sign"></i>
202
+ # </a>
203
+ # <% end %>
204
+ #
205
+ # This modification allows the `:description` tooltip to work with your chosen UI and JavaScript library.
206
+ # The same technique can be applied to filters by calling `filter.options` in corresponding partials.
207
+ #
208
+ # ## Highlight Rows
209
+ #
210
+ # To add custom HTML classes to each row for styling, modify the `_row.html.erb` partial:
211
+ #
212
+ # ``` diff
213
+ # -<tr>
214
+ # +<tr class="<%= grid.respond_to?(:row_class) ? grid.row_class(asset) : "" %>">
215
+ # <% grid.html_columns(*options[:columns]).each do |column| %>
216
+ # <td class="<%= datagrid_column_classes(grid, column) %>">
217
+ # <%= datagrid_value(grid, column, asset) %>
218
+ # </td>
219
+ # <% end %>
220
+ # ```
221
+ #
222
+ # This allows you to define a custom `row_class` method in your grid class, like this:
223
+ #
224
+ # class IssuesGrid < ApplicationGrid
225
+ # scope { Issue }
226
+ #
227
+ # def row_class(issue)
228
+ # case issue.status
229
+ # when "fixed" then "green"
230
+ # when "rejected" then "red"
231
+ # else "blue"
232
+ # end
233
+ # end
234
+ # end
235
+ #
236
+ # ## Localization
237
+ #
238
+ # You can overwrite Datagrid’s custom localization keys at the application level.
239
+ # See the localization keys here:
240
+ #
241
+ # https://github.com/bogdan/datagrid/blob/master/lib/datagrid/locale/en.yml
5
242
  module Helper
6
-
7
243
  # @param grid [Datagrid] grid object
8
244
  # @param column [Datagrid::Columns::Column, String, Symbol] column name
9
245
  # @param model [Object] an object from grid scope
@@ -15,7 +251,9 @@ module Datagrid
15
251
  # <% end %>
16
252
  # </ul>
17
253
  def datagrid_value(grid, column, model)
18
- datagrid_renderer.format_value(grid, column, model)
254
+ column = grid.column_by_name(column) if column.is_a?(String) || column.is_a?(Symbol)
255
+
256
+ grid.html_value(column, self, model)
19
257
  end
20
258
 
21
259
  # @!visibility private
@@ -38,12 +276,20 @@ module Datagrid
38
276
  # Default: 'datagrid'.
39
277
  # @param grid [Datagrid] grid object
40
278
  # @param assets [Array] objects from grid scope
279
+ # @param [Hash{Symbol => Object}] options HTML attributes to be passed to `<table>` tag
41
280
  # @return [String] table tag HTML markup
42
281
  # @example
43
282
  # assets = grid.assets.page(params[:page])
44
283
  # datagrid_table(grid, assets, options)
45
284
  def datagrid_table(grid, assets = grid.assets, **options)
46
- datagrid_renderer.table(grid, assets, **options)
285
+ _render_partial(
286
+ "table", options[:partials],
287
+ {
288
+ grid: grid,
289
+ options: options,
290
+ assets: assets,
291
+ },
292
+ )
47
293
  end
48
294
 
49
295
  # Renders HTML table header for given grid instance using columns defined in it
@@ -58,11 +304,19 @@ module Datagrid
58
304
  # * <tt>:partials</tt> - Path for partials lookup.
59
305
  # Default: 'datagrid'.
60
306
  # @param grid [Datagrid] grid object
307
+ # @param [Object] opts (deprecated) pass keyword arguments instead
308
+ # @param [Hash] options
61
309
  # @return [String] HTML table header tag markup
62
- def datagrid_header(grid, options = {})
63
- datagrid_renderer.header(grid, options)
64
- end
310
+ def datagrid_header(grid, opts = :__unspecified__, **options)
311
+ unless opts == :__unspecified__
312
+ Datagrid::Utils.warn_once("datagrid_header now requires ** operator when passing options.")
313
+ options.reverse_merge!(opts)
314
+ end
315
+ options[:order] = true unless options.key?(:order)
65
316
 
317
+ _render_partial("head", options[:partials],
318
+ { grid: grid, options: options },)
319
+ end
66
320
 
67
321
  # Renders HTML table rows using given grid definition using columns defined in it.
68
322
  # Allows to provide a custom layout for each for in place with a block
@@ -75,6 +329,7 @@ module Datagrid
75
329
  # * <tt>:partials</tt> - Path for partials lookup.
76
330
  # Default: 'datagrid'.
77
331
  #
332
+ # @return [String]
78
333
  # @example
79
334
  # = datagrid_rows(grid) # Generic table rows Layout
80
335
  #
@@ -83,30 +338,73 @@ module Datagrid
83
338
  # %td= row.project_name
84
339
  # %td.project-status{class: row.status}= row.status
85
340
  def datagrid_rows(grid, assets = grid.assets, **options, &block)
86
- datagrid_renderer.rows(grid, assets, **options, &block)
341
+ safe_join(
342
+ assets.map do |asset|
343
+ datagrid_row(grid, asset, **options, &block)
344
+ end.to_a,
345
+ )
87
346
  end
88
347
 
89
- # Renders ordering controls for the given column name
348
+ # @return [String] renders ordering controls for the given column name
90
349
  #
91
350
  # Supported options:
92
351
  #
93
352
  # * <tt>:partials</tt> - Path for partials lookup.
94
353
  # Default: 'datagrid'.
95
354
  def datagrid_order_for(grid, column, options = {})
96
- datagrid_renderer.order_for(grid, column, options)
355
+ Datagrid::Utils.warn_once(<<~MSG)
356
+ datagrid_order_for is deprecated.
357
+ Put necessary code inline inside datagrid/head partial.
358
+ See built-in partial for example.
359
+ MSG
360
+ _render_partial("order_for", options[:partials],
361
+ { grid: grid, column: column },)
362
+ end
363
+
364
+ # Renders HTML for grid with all filters inputs and labels defined in it
365
+ #
366
+ # Supported options:
367
+ #
368
+ # * <tt>:partials</tt> - Path for form partial lookup.
369
+ # Default: 'datagrid' results in using `app/views/datagrid/` partials.
370
+ # Example: 'datagrid_admin' results in using `app/views/datagrid_admin` partials.
371
+ # * <tt>:model</tt> - Datagrid object to be rendedred.
372
+ # * All options supported by Rails <tt>form_with</tt> helper
373
+ # @param grid [Datagrid] grid object
374
+ # @param [Hash{Symbol => Object}] options
375
+ # @return [String] form HTML tag markup
376
+ def datagrid_form_with(**options)
377
+ raise ArgumentError, "datagrid_form_with block argument is invalid. Use form_with instead." if block_given?
378
+
379
+ grid = options[:model]
380
+ raise ArgumentError, "Grid has no available filters" if grid&.filters&.empty?
381
+
382
+ _render_partial("form", options[:partials], { grid: options[:model], options: options })
97
383
  end
98
384
 
99
- # Renders HTML for for grid with all filters inputs and lables defined in it
385
+ # Renders HTML for grid with all filters inputs and labels defined in it
100
386
  #
101
387
  # Supported options:
102
388
  #
103
389
  # * <tt>:partials</tt> - Path for form partial lookup.
104
390
  # Default: 'datagrid'.
105
- # * All options supported by Rails <tt>form_for</tt> helper
391
+ # * All options supported by Rails <tt>form_with</tt> helper
392
+ # @deprecated Use {#datagrid_form_with} instead.
106
393
  # @param grid [Datagrid] grid object
394
+ # @param [Hash] options
107
395
  # @return [String] form HTML tag markup
108
396
  def datagrid_form_for(grid, options = {})
109
- datagrid_renderer.form_for(grid, options)
397
+ Datagrid::Utils.warn_once("datagrid_form_for is deprecated if favor of datagrid_form_with.")
398
+ _render_partial(
399
+ "form", options[:partials],
400
+ grid: grid,
401
+ options: {
402
+ method: :get,
403
+ as: grid.param_name,
404
+ local: true,
405
+ **options,
406
+ },
407
+ )
110
408
  end
111
409
 
112
410
  # Provides access to datagrid columns data.
@@ -114,6 +412,7 @@ module Datagrid
114
412
  # @param grid [Datagrid] grid object
115
413
  # @param asset [Object] object from grid scope
116
414
  # @param block [Proc] block with Datagrid::Helper::HtmlRow as an argument returning a HTML markup as a String
415
+ # @param [Hash{Symbol => Object}] options
117
416
  # @return [Datagrid::Helper::HtmlRow, String] captured HTML markup if block given otherwise row object
118
417
  # @example
119
418
  # # Suppose that grid has first_name and last_name columns
@@ -130,29 +429,105 @@ module Datagrid
130
429
  # @example
131
430
  # <%= datagrid_row(grid, user, columns: [:first_name, :last_name, :actions]) %>
132
431
  def datagrid_row(grid, asset, **options, &block)
133
- datagrid_renderer.row(grid, asset, **options, &block)
432
+ Datagrid::Helper::HtmlRow.new(self, grid, asset, options).tap do |row|
433
+ return capture(row, &block) if block_given?
434
+ end
134
435
  end
135
436
 
136
437
  # Generates an ascending or descending order url for the given column
137
438
  # @param grid [Datagrid] grid object
138
439
  # @param column [Datagrid::Columns::Column, String, Symbol] column name
139
- # @param descending [Boolean] specifies order direction. Ascending if false, otherwise descending.
440
+ # @param descending [Boolean] order direction, descending if true, otherwise ascending.
140
441
  # @return [String] order layout HTML markup
141
442
  def datagrid_order_path(grid, column, descending)
142
- datagrid_renderer.order_path(grid, column, descending, request)
443
+ column = grid.column_by_name(column)
444
+ query = request&.query_parameters || {}
445
+ ActionDispatch::Http::URL.path_for(
446
+ path: request&.path || "/",
447
+ params: query.merge(grid.query_params(order: column.name, descending: descending)),
448
+ )
143
449
  end
144
450
 
451
+ # @!visibility private
452
+ def datagrid_column_classes(grid, column)
453
+ Datagrid::Utils.warn_once(<<~MSG)
454
+ datagrid_column_classes is deprecated. Assign necessary classes manually.
455
+ Correspond to default datagrid/rows partial for example.)
456
+ MSG
457
+ column = grid.column_by_name(column)
458
+ order_class = if grid.ordered_by?(column)
459
+ ["ordered", grid.descending ? "desc" : "asc"]
460
+ end
461
+ class_names(column.name, order_class, column.options[:class], column.tag_options[:class])
462
+ end
145
463
 
146
464
  protected
147
465
 
148
- def datagrid_renderer
149
- Renderer.for(self)
466
+ def _render_partial(partial_name, partials_path, locals = {})
467
+ render({
468
+ partial: File.join(partials_path || "datagrid", partial_name),
469
+ locals: locals,
470
+ })
150
471
  end
151
472
 
152
- def datagrid_column_classes(grid, column)
153
- order_class = grid.ordered_by?(column) ? ["ordered", grid.descending ? "desc" : "asc"] : nil
154
- [column.name, order_class, column.options[:class]].compact.join(" ")
473
+ # Represents a datagrid row that provides access to column values for the given asset
474
+ # @example
475
+ # row = datagrid_row(grid, user)
476
+ # row.class # => Datagrid::Helper::HtmlRow
477
+ # row.first_name # => "<strong>Bogdan</strong>"
478
+ # row.grid # => Grid object
479
+ # row.asset # => User object
480
+ # row.each do |value|
481
+ # puts value
482
+ # end
483
+ class HtmlRow
484
+ include Enumerable
485
+
486
+ attr_reader :grid, :asset, :options
487
+
488
+ # @!visibility private
489
+ def initialize(renderer, grid, asset, options)
490
+ @renderer = renderer
491
+ @grid = grid
492
+ @asset = asset
493
+ @options = options
494
+ end
495
+
496
+ # @return [Object] a column value for given column name
497
+ def get(column)
498
+ @renderer.datagrid_value(@grid, column, @asset)
499
+ end
500
+
501
+ # Iterates over all column values that are available in the row
502
+ # param block [Proc] column value iterator
503
+ def each(&block)
504
+ (@options[:columns] || @grid.html_columns).each do |column|
505
+ block.call(get(column))
506
+ end
507
+ end
508
+
509
+ # @return [String] HTML row format
510
+ def to_s
511
+ @renderer.send(:_render_partial, "row", options[:partials], {
512
+ grid: grid,
513
+ options: options,
514
+ asset: asset,
515
+ },)
516
+ end
517
+
518
+ protected
519
+
520
+ def method_missing(method, *args, &blk)
521
+ if (column = @grid.column_by_name(method))
522
+ get(column)
523
+ else
524
+ super
525
+ end
526
+ end
527
+
528
+ def respond_to_missing?(method, include_private = false)
529
+ !!@grid.column_by_name(method) || super
530
+ end
155
531
  end
156
532
  end
157
533
  end
158
-