effective_datatables 4.9.1 → 4.10.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: afc1825001f74c08289310c87a0e3ffa014a2e909c21487841103e3adf05e0a5
4
- data.tar.gz: 5318e53b07af9daa08cc4fd39f14cf9570620e30f9222beb70b830de47dd0f06
3
+ metadata.gz: 173cdec1d403c8203a7f3d3701104a76d7fd8fb04843c4989180b351b5096264
4
+ data.tar.gz: 70da4dcc029e6f7ac2a2f0d7f896734c4036eea227dfd5ecb6354f1efa59f531
5
5
  SHA512:
6
- metadata.gz: 9c882a77988f466ef89a54361abd2fa26953554b4a859999d05c462e88a8412966ab3292914d28ae22f9a550bcb3eaea85ffa76d24aa5fc6bb16a53c05352801
7
- data.tar.gz: 0a129c87f1bce4334570f80f10ca8ab2b432ede293f9ab34c0e135bdde99e936563864e4cc0abc6f0c679fca2498d3965f8ad5610d941b1512620774264b45e7
6
+ metadata.gz: 474ead17fcde2196a4bdf06cdfa02c9d23e1dc4345126d79bcdf4fa3aa9785a358cce6d54a062862f17ec33c618a908db703a1eaa94e847a87df42dfe738b816
7
+ data.tar.gz: 55570bbe0d91f0bcdb24d818368a3a4dce9be6ffc0d7a99327121d8204df62a56b6eecc61a1bbbb6bb89b1feff5e4ea6d293497bb571ffff640331f14076d8b2
data/README.md CHANGED
@@ -46,6 +46,7 @@ Please check out [Effective Datatables 3.x](https://github.com/code-and-effect/e
46
46
  * [order](#order)
47
47
  * [reorder](#reorder)
48
48
  * [aggregate](#aggregate)
49
+ * [download](#download)
49
50
  * [filters](#filters)
50
51
  * [scope](#scope)
51
52
  * [filter](#filter)
@@ -197,6 +198,13 @@ class PostsDatatable < Effective::Datatable
197
198
  # POSTs to the given url with params[:ids], an Array of ids for all selected rows
198
199
  # These actions are assumed to change the underlying collection
199
200
  bulk_action 'Approve all', bulk_approve_posts_path, data: { confirm: 'Approve all selected posts?' }
201
+ # GETs to the given url. Pass the ids via cookie, encoded in url, or LocalStorage.
202
+ # These actions are assumed to redirect the user to some other page.
203
+ bulk_action 'Action 1 | ids encoded in params', action_1_posts_path, data: { method: :get }
204
+
205
+ bulk_action 'Action 2 | ids stored in _ids_ field in local storage', action_2_posts_path, data: { 'payload-mode' => 'local-storage', method: :get }
206
+
207
+ bulk_action 'Action 3 | ids stored in _ids_ field in a cookie', action_3_posts_path, data: { 'payload-mode' => 'cookie', method: :get }
200
208
  bulk_action_divider
201
209
  bulk_action 'Destroy all', bulk_destroy_posts_path, data: { confirm: 'Destroy all selected posts?' }
202
210
  end
@@ -674,6 +682,30 @@ end.aggregate { |values, column| distance_of_time_in_words(values.min, values.ma
674
682
 
675
683
  In the above example, `values` is an Array containing all row's values for one column at a time.
676
684
 
685
+ ## download
686
+
687
+ Add a Download button which streams a CSV file containing all rows and columns in the table's collection, ignoring any search, sort or filtering.
688
+
689
+ This is an opt-in feature.
690
+
691
+ To enable, please set `config.download = true` in your `config/initializers/effective_datatables.rb` file.
692
+
693
+ Once enabled, you can disable it on an individual table by:
694
+
695
+ ```ruby
696
+ datatable do
697
+ download false
698
+ end
699
+ ```
700
+
701
+ and you can exclude individual columns from being rendered on the CSV export
702
+
703
+ ```ruby
704
+ col :first_name, csv: false
705
+ ```
706
+
707
+ The column will still appear in the export, but the contents will be blank.
708
+
677
709
  ## filters
678
710
 
679
711
  Creates a single form with fields for each `filter` and a single radio input field for all `scopes`.
@@ -772,6 +804,12 @@ You can also specify `data-method: :get` to instead make a `GET` request with th
772
804
  ```ruby
773
805
  bulk_actions do
774
806
  bulk_action 'Approve all', bulk_approve_posts_path, data: { confirm: 'Approve all selected posts?' }
807
+
808
+ bulk_action 'Action 1 | ids encoded in params', action_1_posts_path, data: { method: :get }
809
+
810
+ bulk_action 'Action 2 | ids stored in _ids_ field in local storage', action_2_posts_path, data: { 'payload-mode' => 'local-storage', method: :get }
811
+
812
+ bulk_action 'Action 3 | ids stored in _ids_ field in a cookie', action_3_posts_path, data: { 'payload-mode' => 'cookie', method: :get }
775
813
  end
776
814
  ```
777
815
 
@@ -781,6 +819,9 @@ In your `routes` file:
781
819
  resources :posts do
782
820
  collection do
783
821
  post :bulk_approve
822
+ get :action_1
823
+ get :action_2
824
+ get :action_3
784
825
  end
785
826
  end
786
827
  ```
@@ -799,6 +840,22 @@ def bulk_approve
799
840
  render json: { status: 500, message: 'An error occured while approving a post.' }
800
841
  end
801
842
  end
843
+
844
+ def action_1
845
+ @posts = Post.where(id: params[:ids])
846
+
847
+ render :some_partial
848
+ end
849
+
850
+ def action_2
851
+ @posts = Post.where(id: cookies[:ids].split(','))
852
+
853
+ render :some_partial
854
+ end
855
+
856
+ def action_3
857
+ render :some_partial # and get ids via JS: localStorage.getItem('ids');
858
+ end
802
859
  ```
803
860
 
804
861
  or if using [effective_resources](https://github.com/code-and-effect/effective_resources):
@@ -41,11 +41,25 @@ restoreSelected = ($table, selected) ->
41
41
 
42
42
  if present then $bulkActions.removeAttr('disabled') else $bulkActions.attr('disabled', 'disabled')
43
43
 
44
- #### Bulk Action link behaviour
44
+ # rails_ujs data-confirm requires special attention
45
+ # https://github.com/rails/rails/blob/main/actionview/app/assets/javascripts/rails-ujs/features/confirm.coffee
46
+ $(document).on 'confirm:complete', '.dataTables_wrapper .buttons-bulk-actions a', (event) ->
47
+ if event.originalEvent.detail && event.originalEvent.detail[0] == true
48
+ doBulkActionPost(event)
49
+ (window.Rails || $.rails).stopEverything(event)
50
+ return false
51
+
45
52
  $(document).on 'click', '.dataTables_wrapper .buttons-bulk-actions a', (event) ->
46
- event.preventDefault() # prevent the click
53
+ unless $(event.currentTarget).data('confirm')
54
+ doBulkActionPost(event)
55
+ event.preventDefault()
56
+
57
+ doBulkActionPost = (event) ->
58
+ $bulkAction = $(event.currentTarget) # This is the regular <a href=...> tag maybe with data-confirm
59
+
60
+ document.cookie = 'ids=; expires=Thu, 01 Jan 1970 00:00:00 GMT'
61
+ localStorage.removeItem('ids')
47
62
 
48
- $bulkAction = $(event.currentTarget) # This is a regular <a href=...> tag
49
63
  $wrapper = $bulkAction.closest('.dataTables_wrapper')
50
64
  $table = $wrapper.find('table.dataTable').first()
51
65
  $processing = $table.siblings('.dataTables_processing').first()
@@ -54,6 +68,7 @@ $(document).on 'click', '.dataTables_wrapper .buttons-bulk-actions a', (event) -
54
68
  url = $bulkAction.attr('href')
55
69
  title = $bulkAction.text()
56
70
  download = $bulkAction.data('bulk-download')
71
+ payload_mode = $bulkAction.data('payload-mode')
57
72
  token = $table.data('authenticity-token')
58
73
  values = $.map($selected, (input) -> input.getAttribute('value'))
59
74
  method = $bulkAction.data('ajax-method')
@@ -61,10 +76,17 @@ $(document).on 'click', '.dataTables_wrapper .buttons-bulk-actions a', (event) -
61
76
  return unless url && values
62
77
 
63
78
  if method == 'GET'
64
- if url.includes('?')
65
- window.location.assign(url + '&' + $.param({ids: values}))
79
+ if payload_mode == 'cookie'
80
+ document.cookie = "ids=#{values}";
81
+ window.location.assign(url)
82
+ else if payload_mode == 'local-storage'
83
+ localStorage.setItem('ids', values);
84
+ window.location.assign(url)
66
85
  else
67
- window.location.assign(url + '?' + $.param({ids: values}))
86
+ if url.includes('?')
87
+ window.location.assign(url + '&' + $.param({ids: values}))
88
+ else
89
+ window.location.assign(url + '?' + $.param({ids: values}))
68
90
 
69
91
  return
70
92
 
@@ -0,0 +1,10 @@
1
+ $(document).on 'click', '.dataTables_wrapper a.buttons-download', (event) ->
2
+ $button = $(event.currentTarget)
3
+ $table = $('#' + $button.attr('aria-controls'))
4
+
5
+ url = $table.data('source').replace('.json', '/download.csv')
6
+ attributes = 'attributes=' + encodeURIComponent($table.data('attributes'))
7
+
8
+ $button.attr('href', url + '?' + attributes)
9
+
10
+ setTimeout (=> $button.attr('href', 'download.csv')), 0
@@ -112,14 +112,8 @@ initializeDataTables = (target) ->
112
112
  $table = $(api.table().node())
113
113
  $buttons = $table.closest('.dataTables_wrapper').children().first().find('.dt-buttons')
114
114
 
115
- if $table.data('reset')
116
- $buttons.prepend($table.data('reset'))
117
-
118
- if $table.data('reorder')
119
- $buttons.prepend($table.data('reorder'))
120
-
121
- if $table.data('bulk-actions')
122
- $buttons.prepend($table.data('bulk-actions'))
115
+ if $table.data('buttons-html')
116
+ $buttons.prepend($table.data('buttons-html'))
123
117
 
124
118
  drawAggregates = ($table, aggregates) ->
125
119
  $tfoot = $table.find('tfoot').first()
@@ -1,3 +1,5 @@
1
+ require 'csv'
2
+
1
3
  module Effective
2
4
  class DatatablesController < ApplicationController
3
5
  skip_log_page_views quiet: true if defined?(EffectiveLogging)
@@ -20,6 +22,36 @@ module Effective
20
22
  end
21
23
  end
22
24
 
25
+ def download
26
+ @datatable = EffectiveDatatables.find(params[:id], params[:attributes])
27
+ @datatable.view = view_context
28
+
29
+ EffectiveDatatables.authorize!(self, :index, @datatable.collection_class)
30
+
31
+ respond_to do |format|
32
+ format.csv do
33
+ headers.delete('Content-Length')
34
+
35
+ headers['X-Accel-Buffering'] = 'no'
36
+ headers['Cache-Control'] = 'no-cache'
37
+ headers["Content-Type"] = @datatable.csv_content_type
38
+ headers["Content-Disposition"] = %(attachment; filename="#{@datatable.csv_filename}")
39
+ headers['Last-Modified'] = Time.zone.now.ctime.to_s
40
+
41
+ self.response_body = @datatable.csv_stream
42
+ response.status = 200
43
+ end
44
+
45
+ # format.csv do
46
+ # send_data(@datatable.csv_file, filename: @datatable.csv_filename, type: @datatable.csv_content_type, disposition: 'attachment')
47
+ # end
48
+
49
+ format.all do
50
+ render(status: :unauthorized, body: 'Access Denied')
51
+ end
52
+ end
53
+ end
54
+
23
55
  def reorder
24
56
  begin
25
57
  @datatable = EffectiveDatatables.find(params[:id], params[:attributes])
@@ -1,15 +1,21 @@
1
1
  # These are expected to be called by a developer. They are part of the datatables DSL.
2
2
  module EffectiveDatatablesHelper
3
- def render_datatable(datatable, input_js: {}, buttons: true, charts: true, entries: true, filters: true, inline: false, namespace: nil, pagination: true, search: true, simple: false, sort: true)
3
+ def render_datatable(datatable, input_js: {}, buttons: true, charts: true, download: nil, entries: true, filters: true, inline: false, namespace: nil, pagination: true, search: true, simple: false, sort: true)
4
4
  raise 'expected datatable to be present' unless datatable
5
5
  raise 'expected input_js to be a Hash' unless input_js.kind_of?(Hash)
6
6
 
7
+ if download.nil?
8
+ download = (buttons && EffectiveDatatables.download)
9
+ end
10
+
7
11
  if simple
8
- buttons = charts = entries = filters = pagination = search = sort = false
12
+ buttons = charts = download = entries = filters = pagination = search = sort = false
9
13
  end
10
14
 
11
15
  datatable.attributes[:inline] = true if inline
12
16
  datatable.attributes[:sortable] = false unless sort
17
+ datatable.attributes[:searchable] = false unless search
18
+ datatable.attributes[:downloadable] = false unless download
13
19
  datatable.attributes[:namespace] = namespace if namespace
14
20
 
15
21
  datatable.view ||= self
@@ -44,7 +50,7 @@ module EffectiveDatatablesHelper
44
50
  'all-label' => I18n.t('effective_datatables.all'),
45
51
  'attributes' => EffectiveDatatables.encrypt(datatable.attributes),
46
52
  'authenticity-token' => form_authenticity_token,
47
- 'bulk-actions' => datatable_bulk_actions(datatable),
53
+ 'buttons-html' => datatable_buttons(datatable),
48
54
  'columns' => datatable_columns(datatable),
49
55
  'default-visibility' => datatable.default_visibility.to_json,
50
56
  'display-length' => datatable.display_length,
@@ -54,8 +60,6 @@ module EffectiveDatatablesHelper
54
60
  'inline' => inline.to_s,
55
61
  'language' => EffectiveDatatables.language(I18n.locale),
56
62
  'options' => input_js.to_json,
57
- 'reset' => (datatable_reset(datatable) if search),
58
- 'reorder' => datatable_reorder(datatable),
59
63
  'reorder-index' => (datatable.columns[:_reorder][:index] if datatable.reorder?).to_s,
60
64
  'simple' => simple.to_s,
61
65
  'spinner' => icon('spinner'), # effective_bootstrap
@@ -64,7 +68,7 @@ module EffectiveDatatablesHelper
64
68
  }
65
69
  }
66
70
 
67
- if (charts || filters)
71
+ retval = if (charts || filters)
68
72
  output = ''.html_safe
69
73
 
70
74
  if charts
@@ -85,6 +89,10 @@ module EffectiveDatatablesHelper
85
89
  locals: { datatable: datatable, effective_datatable_params: effective_datatable_params }
86
90
  )
87
91
  end
92
+
93
+ Rails.logger.info(" Rendered datatable #{datatable.class} #{datatable.source_location}")
94
+
95
+ retval
88
96
  end
89
97
 
90
98
  def render_inline_datatable(datatable)
@@ -19,23 +19,12 @@ module EffectiveDatatablesPrivateHelper
19
19
  end.to_json.html_safe
20
20
  end
21
21
 
22
- def datatable_bulk_actions(datatable)
23
- if datatable._bulk_actions.present?
24
- render(partial: '/effective/datatables/bulk_actions_dropdown', locals: { datatable: datatable }).gsub("'", '"').html_safe
25
- end
26
- end
27
-
28
22
  def datatable_display_order(datatable)
29
23
  ((datatable.sortable? && datatable.order_index) ? [datatable.order_index, datatable.order_direction] : false).to_json.html_safe
30
24
  end
31
25
 
32
- def datatable_reset(datatable)
33
- link_to(content_tag(:span, t('effective_datatables.reset')), '#', class: 'btn btn-link btn-sm buttons-reset-search')
34
- end
35
-
36
- def datatable_reorder(datatable)
37
- return unless datatable.reorder? && EffectiveDatatables.authorized?(self, :update, datatable.collection_class)
38
- link_to(content_tag(:span, t('effective_datatables.reorder')), '#', class: 'btn btn-link btn-sm buttons-reorder', disabled: true)
26
+ def datatable_buttons(datatable, search: true)
27
+ render('/effective/datatables/buttons', datatable: datatable, search: search).gsub("'", '"').html_safe
39
28
  end
40
29
 
41
30
  def datatable_new_resource_button(datatable, name, column)
@@ -21,12 +21,16 @@ module Effective
21
21
  # The view
22
22
  attr_reader :view
23
23
 
24
+ # Set by DSL so we can track where this datatable is coming from
25
+ attr_accessor :source_location
26
+
24
27
  extend Effective::EffectiveDatatable::Dsl
25
28
 
26
29
  include Effective::EffectiveDatatable::Attributes
27
30
  include Effective::EffectiveDatatable::Collection
28
31
  include Effective::EffectiveDatatable::Compute
29
32
  include Effective::EffectiveDatatable::Cookie
33
+ include Effective::EffectiveDatatable::Csv
30
34
  include Effective::EffectiveDatatable::Format
31
35
  include Effective::EffectiveDatatable::Hooks
32
36
  include Effective::EffectiveDatatable::Params
@@ -137,6 +141,10 @@ module Effective
137
141
  to_json[:recordsTotal] == 0
138
142
  end
139
143
 
144
+ def to_csv
145
+ csv_file()
146
+ end
147
+
140
148
  def to_json
141
149
  @json ||= (
142
150
  {
@@ -164,6 +172,14 @@ module Effective
164
172
  !reorder? && attributes[:sortable] != false
165
173
  end
166
174
 
175
+ def searchable?
176
+ attributes[:searchable] != false
177
+ end
178
+
179
+ def downloadable?
180
+ attributes[:downloadable] != false
181
+ end
182
+
167
183
  # Whether the filters must be rendered as a <form> or we can keep the normal <div> behaviour
168
184
  def _filters_form_required?
169
185
  _form[:verb].present?
@@ -58,13 +58,14 @@ module Effective
58
58
  finalize(col)
59
59
  end
60
60
 
61
- def arrayize(collection)
61
+ def arrayize(collection, csv: false)
62
62
  collection.map do |obj|
63
63
  columns.map do |name, opts|
64
- if state[:visible][name] == false && (name != order_name) # Sort by invisible array column
64
+ if state[:visible][name] == false && !csv && (name != order_name) # Sort by invisible array column
65
+ BLANK
66
+ elsif csv && !opts[:csv]
65
67
  BLANK
66
68
  elsif opts[:compute]
67
-
68
69
  if array_collection?
69
70
  dsl_tool.instance_exec(obj, obj[opts[:index]], &opts[:compute])
70
71
  else
@@ -0,0 +1,55 @@
1
+ require 'csv'
2
+
3
+ module Effective
4
+ module EffectiveDatatable
5
+ module Csv
6
+
7
+ def csv_filename
8
+ self.class.name.underscore.parameterize + '.csv'
9
+ end
10
+
11
+ def csv_content_type
12
+ 'text/csv; charset=utf-8'
13
+ end
14
+
15
+ def csv_header
16
+ columns.map { |_, opts| opts[:label] || '' }
17
+ end
18
+
19
+ def csv_file
20
+ CSV.generate do |csv|
21
+ csv << csv_header()
22
+
23
+ collection.send(csv_collection_method) do |resources|
24
+ resources = arrayize(resources, csv: true)
25
+ format(resources, csv: true)
26
+ finalize(resources)
27
+
28
+ resources.each { |resource| csv << resource }
29
+ end
30
+ end
31
+ end
32
+
33
+ def csv_stream
34
+ EffectiveResources.with_resource_enumerator do |lines|
35
+ lines << CSV.generate_line(csv_header)
36
+
37
+ collection.public_send(csv_collection_method) do |resources|
38
+ resources = arrayize(resources, csv: true)
39
+ format(resources, csv: true)
40
+ finalize(resources)
41
+
42
+ resources.each { |resource| lines << CSV.generate_line(resource) }
43
+ end
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def csv_collection_method
50
+ (active_record_collection? ? :find_in_batches : :to_a)
51
+ end
52
+
53
+ end
54
+ end
55
+ end
@@ -24,11 +24,15 @@ module Effective
24
24
  reorder_col(name)
25
25
  end
26
26
 
27
+ def download(bool)
28
+ datatable.attributes[:downloadable] = bool
29
+ end
30
+
27
31
  # A col has its internal values sorted/searched before the block is run
28
32
  # Anything done in the block, is purely a format on the after sorted/ordered value
29
33
  # the original object == the computed value, which is yielded to the format block
30
34
  # You can't do compute with .col
31
- def col(name, action: nil, as: nil, col_class: nil, label: nil, partial: nil, partial_as: nil, responsive: 10000, search: {}, sort: true, sql_column: nil, th: nil, th_append: nil, visible: true, &format)
35
+ def col(name, action: nil, as: nil, col_class: nil, csv: true, label: nil, partial: nil, partial_as: nil, responsive: 10000, search: {}, sort: true, sql_column: nil, th: nil, th_append: nil, visible: true, &format)
32
36
  raise 'You cannot use partial: ... with the block syntax' if partial && block_given?
33
37
 
34
38
  name = name.to_sym unless name.to_s.include?('.')
@@ -38,6 +42,7 @@ module Effective
38
42
  as: as,
39
43
  compute: nil,
40
44
  col_class: col_class,
45
+ csv: csv,
41
46
  format: (format if block_given?),
42
47
  index: nil,
43
48
  label: (label.nil? ? name.to_s.split('.').last.titleize : label),
@@ -56,7 +61,7 @@ module Effective
56
61
 
57
62
  # A val is a computed value that is then sorted/searched after the block is run
58
63
  # You can have another block by calling .format afterwards to work on the computed value itself
59
- def val(name, action: nil, as: nil, col_class: nil, label: nil, partial: nil, partial_as: nil, responsive: 10000, search: {}, sort: true, sql_column: false, th: nil, th_append: nil, visible: true, &compute)
64
+ def val(name, action: nil, as: nil, col_class: nil, csv: true, label: nil, partial: nil, partial_as: nil, responsive: 10000, search: {}, sort: true, sql_column: false, th: nil, th_append: nil, visible: true, &compute)
60
65
  raise 'You cannot use partial: ... with the block syntax' if partial && block_given?
61
66
 
62
67
  name = name.to_sym unless name.to_s.include?('.')
@@ -66,6 +71,7 @@ module Effective
66
71
  as: as,
67
72
  compute: (compute if block_given?),
68
73
  col_class: col_class,
74
+ csv: csv,
69
75
  format: nil,
70
76
  index: nil,
71
77
  label: (label.nil? ? name.to_s.split('.').last.titleize : label),
@@ -91,6 +97,7 @@ module Effective
91
97
  compute: nil,
92
98
  btn_class: (btn_class || 'btn-sm btn-outline-primary'),
93
99
  col_class: col_class,
100
+ csv: false,
94
101
  format: (format if block_given?),
95
102
  index: nil,
96
103
  inline: (inline.nil? ? datatable.inline? : inline),
@@ -130,6 +137,7 @@ module Effective
130
137
  as: :bulk_actions,
131
138
  compute: nil,
132
139
  col_class: col_class,
140
+ csv: false,
133
141
  format: nil,
134
142
  index: nil,
135
143
  input_name: (input_name || 'bulk_actions_resources'),
@@ -157,6 +165,7 @@ module Effective
157
165
  as: :reorder,
158
166
  compute: nil,
159
167
  col_class: col_class,
168
+ csv: false,
160
169
  format: nil,
161
170
  index: nil,
162
171
  label: false,
@@ -24,6 +24,8 @@ module Effective
24
24
  dsl_tool.in_datatables_do_block = true
25
25
  dsl_tool.instance_exec(&block)
26
26
  dsl_tool.in_datatables_do_block = false
27
+
28
+ self.source_location = block.source_location.first if block.respond_to?(:source_location)
27
29
  end
28
30
  end
29
31
 
@@ -8,14 +8,16 @@ module Effective
8
8
 
9
9
  private
10
10
 
11
- def format(collection)
11
+ def format(collection, csv: false)
12
12
  # We want to use the render :collection for each column that renders partials
13
13
  rendered = {}
14
14
 
15
15
  columns.each do |name, opts|
16
- next unless state[:visible][name]
17
-
18
- if opts[:partial]
16
+ if state[:visible][name] == false && !csv
17
+ # Nothing to do
18
+ elsif csv && !opts[:csv]
19
+ # Nothing to do
20
+ elsif opts[:partial]
19
21
  locals = { datatable: self, column: opts }.merge!(resource_col_locals(opts))
20
22
 
21
23
  rendered[name] = (view.render(
@@ -26,6 +28,7 @@ module Effective
26
28
  locals: locals,
27
29
  spacer_template: SPACER_TEMPLATE
28
30
  ) || '').split(SPACER)
31
+
29
32
  elsif opts[:as] == :actions # This is actions_col and actions_col do .. end, but not actions_col partial: 'something'
30
33
  locals = { datatable: self, column: opts, spacer_template: SPACER_TEMPLATE }
31
34
 
@@ -45,7 +48,6 @@ module Effective
45
48
  else
46
49
  (view.render_resource_actions(collection.map { |row| row[opts[:index]] }, atts, &opts[:format]) || '').split(SPACER)
47
50
  end
48
-
49
51
  end
50
52
  end
51
53
 
@@ -54,9 +56,11 @@ module Effective
54
56
  index = opts[:index]
55
57
  value = row[index]
56
58
 
57
- row[index] = (
58
- if state[:visible][name] == false
59
+ formatted = (
60
+ if state[:visible][name] == false && !csv
59
61
  NONVISIBLE
62
+ elsif csv && !opts[:csv]
63
+ BLANK
60
64
  elsif opts[:as] == :actions
61
65
  rendered[name][row_index]
62
66
  elsif opts[:format] && rendered.key?(name)
@@ -66,14 +70,23 @@ module Effective
66
70
  elsif opts[:partial]
67
71
  rendered[name][row_index]
68
72
  else
69
- format_column(value, opts)
73
+ format_column(value, opts, csv: csv)
70
74
  end
71
75
  )
76
+
77
+ if csv && (opts[:format] || opts[:partial])
78
+ formatted = view.strip_tags(formatted)
79
+
80
+ formatted.gsub!("\n\n", ' ') unless formatted.frozen?
81
+ formatted.gsub!("\n", '') unless formatted.frozen?
82
+ end
83
+
84
+ row[index] = formatted
72
85
  end
73
86
  end
74
87
  end
75
88
 
76
- def format_column(value, column)
89
+ def format_column(value, column, csv: false)
77
90
  return if value.nil? || (column[:resource] && value.blank?)
78
91
 
79
92
  unless column[:as] == :email
@@ -90,7 +103,11 @@ module Effective
90
103
  when :date
91
104
  value.respond_to?(:strftime) ? value.strftime(EffectiveDatatables.format_date) : BLANK
92
105
  when :datetime
93
- value.respond_to?(:strftime) ? value.strftime(EffectiveDatatables.format_datetime) : BLANK
106
+ if csv
107
+ value
108
+ else
109
+ value.respond_to?(:strftime) ? value.strftime(EffectiveDatatables.format_datetime) : BLANK
110
+ end
94
111
  when :decimal
95
112
  value
96
113
  when :duration
@@ -102,7 +119,7 @@ module Effective
102
119
  when :effective_roles
103
120
  value.join(', ')
104
121
  when :email
105
- view.mail_to(value)
122
+ csv ? value : view.mail_to(value)
106
123
  when :integer
107
124
  value
108
125
  when :percent
@@ -0,0 +1,14 @@
1
+ - if datatable._bulk_actions.present?
2
+ = render('/effective/datatables/bulk_actions_dropdown', datatable: datatable)
3
+
4
+ - if datatable.searchable?
5
+ = link_to '#', class: 'btn btn-link btn-sm buttons-reset-search' do
6
+ %span= t('effective_datatables.reset')
7
+
8
+ - if datatable.reorder? && EffectiveDatatables.authorized?(self, :update, datatable.collection_class)
9
+ = link_to '#', class: 'btn btn-link btn-sm buttons-reorder' do
10
+ %span= t('effective_datatables.reorder')
11
+
12
+ - if datatable.downloadable?
13
+ = link_to 'download.csv', class: 'btn btn-link btn-sm buttons-download', 'aria-controls': datatable.to_param do
14
+ %span= t('effective_datatables.download')
@@ -43,4 +43,7 @@ EffectiveDatatables.setup do |config|
43
43
  config.format_datetime = '%F %H:%M'
44
44
  config.format_date = '%F'
45
45
  config.format_time = '%H:%M'
46
+
47
+ # Enable the Download button which serves a CSV of your collection
48
+ config.download = false
46
49
  end
@@ -2,6 +2,7 @@
2
2
  en:
3
3
  effective_datatables:
4
4
  apply: Apply
5
+ download: Download
5
6
  reset: Reset
6
7
  reorder: Reorder
7
8
  new: New
@@ -2,6 +2,7 @@
2
2
  es:
3
3
  effective_datatables:
4
4
  apply: Filtrar
5
+ download: Download
5
6
  reset: Reiniciar
6
7
  reorder: Reordenar
7
8
  new: Nuevo
@@ -2,6 +2,7 @@
2
2
  nl:
3
3
  effective_datatables:
4
4
  apply: Toepassen
5
+ download: Download
5
6
  reset: Reset
6
7
  reorder: Opnieuw ordenen
7
8
  new: Nieuwe
data/config/routes.rb CHANGED
@@ -2,6 +2,7 @@ EffectiveDatatables::Engine.routes.draw do
2
2
  scope :module => 'effective' do
3
3
  match 'datatables/:id(.:format)', to: 'datatables#show', via: [:get, :post], as: :datatable
4
4
  match 'datatables/:id/reorder(.:format)', to: 'datatables#reorder', via: [:post], as: :reorder_datatable
5
+ match 'datatables/:id/download(.:format)', to: 'datatables#download', via: :get, as: :download_datatable
5
6
  end
6
7
  end
7
8
 
@@ -1,3 +1,3 @@
1
1
  module EffectiveDatatables
2
- VERSION = '4.9.1'.freeze
2
+ VERSION = '4.10.0'.freeze
3
3
  end
@@ -21,6 +21,7 @@ module EffectiveDatatables
21
21
  mattr_accessor :format_time
22
22
 
23
23
  mattr_accessor :debug
24
+ mattr_accessor :download
24
25
 
25
26
  alias_method :max_cookie_size, :cookie_max_size
26
27
  alias_method :max_cookie_size=, :cookie_max_size=
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: effective_datatables
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.9.1
4
+ version: 4.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Code and Effect
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-23 00:00:00.000000000 Z
11
+ date: 2021-12-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -111,6 +111,7 @@ files:
111
111
  - app/assets/javascripts/effective_datatables.js
112
112
  - app/assets/javascripts/effective_datatables/bulk_actions.js.coffee
113
113
  - app/assets/javascripts/effective_datatables/charts.js.coffee
114
+ - app/assets/javascripts/effective_datatables/download.js.coffee
114
115
  - app/assets/javascripts/effective_datatables/events.js.coffee
115
116
  - app/assets/javascripts/effective_datatables/filters.js.coffee
116
117
  - app/assets/javascripts/effective_datatables/flash.js.coffee
@@ -141,6 +142,7 @@ files:
141
142
  - app/models/effective/effective_datatable/collection.rb
142
143
  - app/models/effective/effective_datatable/compute.rb
143
144
  - app/models/effective/effective_datatable/cookie.rb
145
+ - app/models/effective/effective_datatable/csv.rb
144
146
  - app/models/effective/effective_datatable/dsl.rb
145
147
  - app/models/effective/effective_datatable/dsl/bulk_actions.rb
146
148
  - app/models/effective/effective_datatable/dsl/charts.rb
@@ -154,6 +156,7 @@ files:
154
156
  - app/views/effective/datatables/_active_storage_column.html.haml
155
157
  - app/views/effective/datatables/_bulk_actions_column.html.haml
156
158
  - app/views/effective/datatables/_bulk_actions_dropdown.html.haml
159
+ - app/views/effective/datatables/_buttons.html.haml
157
160
  - app/views/effective/datatables/_chart.html.haml
158
161
  - app/views/effective/datatables/_datatable.html.haml
159
162
  - app/views/effective/datatables/_filters.html.haml