effective_datatables 4.9.1 → 4.10.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.
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