effective_datatables 4.9.4 → 4.10.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +31 -0
  3. data/app/assets/javascripts/effective_datatables/download.js.coffee +10 -0
  4. data/app/assets/javascripts/effective_datatables/initialize.js.coffee +2 -8
  5. data/app/controllers/effective/datatables_controller.rb +34 -0
  6. data/app/helpers/effective_datatables_controller_helper.rb +2 -0
  7. data/app/helpers/effective_datatables_helper.rb +11 -5
  8. data/app/helpers/effective_datatables_private_helper.rb +13 -22
  9. data/app/models/effective/datatable.rb +16 -1
  10. data/app/models/effective/datatable_column.rb +2 -0
  11. data/app/models/effective/datatable_column_tool.rb +2 -0
  12. data/app/models/effective/datatable_dsl_tool.rb +2 -0
  13. data/app/models/effective/datatable_value_tool.rb +2 -0
  14. data/app/models/effective/effective_datatable/attributes.rb +20 -0
  15. data/app/models/effective/effective_datatable/collection.rb +3 -1
  16. data/app/models/effective/effective_datatable/compute.rb +6 -3
  17. data/app/models/effective/effective_datatable/cookie.rb +2 -0
  18. data/app/models/effective/effective_datatable/csv.rb +57 -0
  19. data/app/models/effective/effective_datatable/dsl/bulk_actions.rb +2 -0
  20. data/app/models/effective/effective_datatable/dsl/charts.rb +2 -0
  21. data/app/models/effective/effective_datatable/dsl/datatable.rb +13 -2
  22. data/app/models/effective/effective_datatable/dsl/filters.rb +2 -0
  23. data/app/models/effective/effective_datatable/dsl.rb +5 -3
  24. data/app/models/effective/effective_datatable/format.rb +34 -11
  25. data/app/models/effective/effective_datatable/hooks.rb +2 -0
  26. data/app/models/effective/effective_datatable/params.rb +2 -0
  27. data/app/models/effective/effective_datatable/resource.rb +2 -0
  28. data/app/models/effective/effective_datatable/state.rb +3 -1
  29. data/app/views/effective/datatables/_buttons.html.haml +14 -0
  30. data/config/effective_datatables.rb +3 -0
  31. data/config/locales/en.yml +1 -0
  32. data/config/locales/es.yml +1 -0
  33. data/config/locales/nl.yml +1 -0
  34. data/config/routes.rb +1 -0
  35. data/lib/effective_datatables/version.rb +1 -1
  36. data/lib/effective_datatables.rb +1 -0
  37. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 768310a5434c778a3e11512125d661bc4ce443a277bb7e15581e50224b81c48f
4
- data.tar.gz: 6b05137c54ef11ce9801b2e1fc48ee525641d2dd5507b7259e80c3387335404b
3
+ metadata.gz: f8fa23825496c27d779457c70827727371d84aeb953b674226d2218d1279555c
4
+ data.tar.gz: dd26ea781b3b09883ee67d156e9407de85400beb0573edb6c98ee782740762cb
5
5
  SHA512:
6
- metadata.gz: edf03e2147e95afd515bc357ac5436dca96c0d3f1aa4fd36ac040bc1cc0885a1a34a0d6ff69897b61bafe8542b46447bc59adc96d42167fd4efc6b98cd65006b
7
- data.tar.gz: 7027f034cee83b584d50290a34674295b3aca4d7b6cb0121ebd1dd052833ffdfaa0be50e26d493d736c8d8e7730aa89dc666b42c0e1219ba33c6ec495f9c01da
6
+ metadata.gz: 5872f805495f9cffcb1e6a41b56671092b0e2a6337d23bc85fb6db5bd90e772dd311c2fb65e21d46fdc42f126906e2b7e7902bef902a1cae6591cb963249e5cc
7
+ data.tar.gz: ceffdf55902c85e2fc8ab99638add52750f9ae09a3fb6e48a284e6db49592cdedae6dca29544486316e96febb5606bfdfcbca015f2fe887aaf5f09b5f96b3a9c
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)
@@ -604,6 +605,12 @@ Any `data-remote` actions will be hijacked and performed as inline ajax by datat
604
605
 
605
606
  If you'd like to opt-out of this behavior, use `actions_col(inline: false)` or add `data-inline: false` to your action link.
606
607
 
608
+ If the automatic actions_col aren't being displayed, try setting the namespace directly when calling the table
609
+
610
+ ```
611
+ MyApp::UsersTable.new(namespace: :my_app)
612
+ ```
613
+
607
614
  ## length
608
615
 
609
616
  Sets the default number of rows per page. Valid lengths are `5`, `10`, `25`, `50`, `100`, `250`, `500`, `:all`
@@ -681,6 +688,30 @@ end.aggregate { |values, column| distance_of_time_in_words(values.min, values.ma
681
688
 
682
689
  In the above example, `values` is an Array containing all row's values for one column at a time.
683
690
 
691
+ ## download
692
+
693
+ 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.
694
+
695
+ This is an opt-in feature.
696
+
697
+ To enable, please set `config.download = true` in your `config/initializers/effective_datatables.rb` file.
698
+
699
+ Once enabled, you can disable it on an individual table by:
700
+
701
+ ```ruby
702
+ datatable do
703
+ download false
704
+ end
705
+ ```
706
+
707
+ and you can exclude individual columns from being rendered on the CSV export
708
+
709
+ ```ruby
710
+ col :first_name, csv: false
711
+ ```
712
+
713
+ The column will still appear in the export, but the contents will be blank.
714
+
684
715
  ## filters
685
716
 
686
717
  Creates a single form with fields for each `filter` and a single radio input field for all `scopes`.
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'csv'
4
+
1
5
  module Effective
2
6
  class DatatablesController < ApplicationController
3
7
  skip_log_page_views quiet: true if defined?(EffectiveLogging)
@@ -20,6 +24,36 @@ module Effective
20
24
  end
21
25
  end
22
26
 
27
+ def download
28
+ @datatable = EffectiveDatatables.find(params[:id], params[:attributes])
29
+ @datatable.view = view_context
30
+
31
+ EffectiveDatatables.authorize!(self, :index, @datatable.collection_class)
32
+
33
+ respond_to do |format|
34
+ format.csv do
35
+ headers.delete('Content-Length')
36
+
37
+ headers['X-Accel-Buffering'] = 'no'
38
+ headers['Cache-Control'] = 'no-cache'
39
+ headers["Content-Type"] = @datatable.csv_content_type
40
+ headers["Content-Disposition"] = %(attachment; filename="#{@datatable.csv_filename}")
41
+ headers['Last-Modified'] = Time.zone.now.ctime.to_s
42
+
43
+ self.response_body = @datatable.csv_stream
44
+ response.status = 200
45
+ end
46
+
47
+ # format.csv do
48
+ # send_data(@datatable.csv_file, filename: @datatable.csv_filename, type: @datatable.csv_content_type, disposition: 'attachment')
49
+ # end
50
+
51
+ format.all do
52
+ render(status: :unauthorized, body: 'Access Denied')
53
+ end
54
+ end
55
+ end
56
+
23
57
  def reorder
24
58
  begin
25
59
  @datatable = EffectiveDatatables.find(params[:id], params[:attributes])
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # These are expected to be called by a developer. They are part of the datatables DSL.
2
4
  module EffectiveDatatablesControllerHelper
3
5
 
@@ -1,15 +1,23 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # These are expected to be called by a developer. They are part of the datatables DSL.
2
4
  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)
5
+ 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
6
  raise 'expected datatable to be present' unless datatable
5
7
  raise 'expected input_js to be a Hash' unless input_js.kind_of?(Hash)
6
8
 
9
+ if download.nil?
10
+ download = (buttons && EffectiveDatatables.download)
11
+ end
12
+
7
13
  if simple
8
- buttons = charts = entries = filters = pagination = search = sort = false
14
+ buttons = charts = download = entries = filters = pagination = search = sort = false
9
15
  end
10
16
 
11
17
  datatable.attributes[:inline] = true if inline
12
18
  datatable.attributes[:sortable] = false unless sort
19
+ datatable.attributes[:searchable] = false unless search
20
+ datatable.attributes[:downloadable] = false unless download
13
21
  datatable.attributes[:namespace] = namespace if namespace
14
22
 
15
23
  datatable.view ||= self
@@ -44,7 +52,7 @@ module EffectiveDatatablesHelper
44
52
  'all-label' => I18n.t('effective_datatables.all'),
45
53
  'attributes' => EffectiveDatatables.encrypt(datatable.attributes),
46
54
  'authenticity-token' => form_authenticity_token,
47
- 'bulk-actions' => datatable_bulk_actions(datatable),
55
+ 'buttons-html' => datatable_buttons(datatable),
48
56
  'columns' => datatable_columns(datatable),
49
57
  'default-visibility' => datatable.default_visibility.to_json,
50
58
  'display-length' => datatable.display_length,
@@ -54,8 +62,6 @@ module EffectiveDatatablesHelper
54
62
  'inline' => inline.to_s,
55
63
  'language' => EffectiveDatatables.language(I18n.locale),
56
64
  'options' => input_js.to_json,
57
- 'reset' => (datatable_reset(datatable) if search),
58
- 'reorder' => datatable_reorder(datatable),
59
65
  'reorder-index' => (datatable.columns[:_reorder][:index] if datatable.reorder?).to_s,
60
66
  'simple' => simple.to_s,
61
67
  'spinner' => icon('spinner'), # effective_bootstrap
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # These aren't expected to be called by a developer. They are internal methods.
2
4
  module EffectiveDatatablesPrivateHelper
3
5
 
@@ -19,23 +21,12 @@ module EffectiveDatatablesPrivateHelper
19
21
  end.to_json.html_safe
20
22
  end
21
23
 
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
24
  def datatable_display_order(datatable)
29
25
  ((datatable.sortable? && datatable.order_index) ? [datatable.order_index, datatable.order_direction] : false).to_json.html_safe
30
26
  end
31
27
 
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)
28
+ def datatable_buttons(datatable, search: true)
29
+ render('/effective/datatables/buttons', datatable: datatable, search: search).gsub("'", '"').html_safe
39
30
  end
40
31
 
41
32
  def datatable_new_resource_button(datatable, name, column)
@@ -44,7 +35,7 @@ module EffectiveDatatablesPrivateHelper
44
35
  action = { action: :new, class: ['btn', column[:btn_class].presence].compact.join(' '), 'data-remote': true }
45
36
 
46
37
  if column[:actions][:new].kind_of?(Hash) # This might be active_record_array_collection?
47
- action.merge!(column[:actions][:new])
38
+ actions = action.merge(column[:actions][:new])
48
39
 
49
40
  effective_resource = (datatable.effective_resource || datatable.fallback_effective_resource)
50
41
  klass = (column[:actions][:new][:klass] || effective_resource&.klass || datatable.collection_class)
@@ -78,13 +69,13 @@ module EffectiveDatatablesPrivateHelper
78
69
  return if opts[:search] == false
79
70
 
80
71
  # Build the search
81
- @_effective_datatables_form_builder || effective_form_with(scope: :datatable_search, url: '#') { |f| @_effective_datatables_form_builder = f }
72
+ @_effective_datatables_form_builder || effective_form_with(scope: 'datatable_search', url: '#') { |f| @_effective_datatables_form_builder = f }
82
73
  form = @_effective_datatables_form_builder
83
74
 
84
75
  collection = opts[:search].delete(:collection)
85
76
  value = datatable.state[:search][name]
86
77
 
87
- options = opts[:search].merge!(
78
+ options = opts[:search].merge(
88
79
  name: nil,
89
80
  feedback: false,
90
81
  label: false,
@@ -98,20 +89,20 @@ module EffectiveDatatablesPrivateHelper
98
89
  when :string, :text, :number
99
90
  form.text_field name, options
100
91
  when :date, :datetime
101
- form.date_field name, options.reverse_merge!(
92
+ form.date_field name, options.reverse_merge(
102
93
  date_linked: false, prepend: false, input_js: { useStrict: true, keepInvalid: true }
103
94
  )
104
95
  when :time
105
- form.time_field name, options.reverse_merge!(
96
+ form.time_field name, options.reverse_merge(
106
97
  date_linked: false, prepend: false, input_js: { useStrict: false, keepInvalid: true }
107
98
  )
108
99
  when :select, :boolean
109
- options[:input_js] = (options[:input_js] || {}).reverse_merge!(placeholder: '')
100
+ options[:input_js] = (options[:input_js] || {}).reverse_merge(placeholder: '')
110
101
 
111
102
  form.select name, collection, options
112
103
  when :bulk_actions
113
104
  options[:data]['role'] = 'bulk-actions'
114
- form.check_box name, options.merge!(label: '&nbsp;')
105
+ form.check_box name, options.merge(label: '&nbsp;')
115
106
  end
116
107
  end
117
108
 
@@ -141,7 +132,7 @@ module EffectiveDatatablesPrivateHelper
141
132
  placeholder: (opts[:label] || name.to_s.titleize),
142
133
  value: value,
143
134
  wrapper: { class: 'form-group col-auto'}
144
- }.merge!(opts.except(:as, :collection, :parse, :value))
135
+ }.merge(opts.except(:as, :collection, :parse, :value))
145
136
 
146
137
  options[:name] = '' unless datatable._filters_form_required?
147
138
 
@@ -173,7 +164,7 @@ module EffectiveDatatablesPrivateHelper
173
164
  label: false,
174
165
  required: false,
175
166
  wrapper: { class: 'form-group col-auto'}
176
- }.merge!(opts.except(:checked, :value))
167
+ }.merge(opts.except(:checked, :value))
177
168
 
178
169
  form.radios :scope, collection, options
179
170
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Effective
2
4
  class Datatable
3
5
  attr_reader :attributes # Anything that we initialize our table with. That's it. Can't be changed by state.
@@ -30,6 +32,7 @@ module Effective
30
32
  include Effective::EffectiveDatatable::Collection
31
33
  include Effective::EffectiveDatatable::Compute
32
34
  include Effective::EffectiveDatatable::Cookie
35
+ include Effective::EffectiveDatatable::Csv
33
36
  include Effective::EffectiveDatatable::Format
34
37
  include Effective::EffectiveDatatable::Hooks
35
38
  include Effective::EffectiveDatatable::Params
@@ -39,7 +42,7 @@ module Effective
39
42
  def initialize(view = nil, attributes = nil)
40
43
  (attributes = view; view = nil) if view.kind_of?(Hash)
41
44
 
42
- @attributes = (attributes || {})
45
+ @attributes = initial_attributes(attributes)
43
46
  @state = initial_state
44
47
 
45
48
  @_aggregates = {}
@@ -140,6 +143,10 @@ module Effective
140
143
  to_json[:recordsTotal] == 0
141
144
  end
142
145
 
146
+ def to_csv
147
+ csv_file()
148
+ end
149
+
143
150
  def to_json
144
151
  @json ||= (
145
152
  {
@@ -167,6 +174,14 @@ module Effective
167
174
  !reorder? && attributes[:sortable] != false
168
175
  end
169
176
 
177
+ def searchable?
178
+ attributes[:searchable] != false
179
+ end
180
+
181
+ def downloadable?
182
+ attributes[:downloadable] != false
183
+ end
184
+
170
185
  # Whether the filters must be rendered as a <form> or we can keep the normal <div> behaviour
171
186
  def _filters_form_required?
172
187
  _form[:verb].present?
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # In practice this is just a regular hash with the aggregate, format, search, sort do syntax that saves a block
2
4
  module Effective
3
5
  class DatatableColumn
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Effective
2
4
  class DatatableColumnTool
3
5
  attr_reader :datatable
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Effective
2
4
 
3
5
  class DatatableDslTool
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Effective
2
4
  # The collection is an Array of Arrays
3
5
  class DatatableValueTool
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Effective
2
4
  module EffectiveDatatable
3
5
  module Attributes
@@ -15,6 +17,24 @@ module Effective
15
17
  @attributes[:namespace] ||= view.controller_path.split('/')[0...-1].join('/')
16
18
  end
17
19
 
20
+ # Polymorphic shorthand attributes.
21
+ # If you pass resource: User(1), it sets resource_id: 1, resource_type: 'User'
22
+ def initial_attributes(attributes)
23
+ return {} if attributes.blank?
24
+
25
+ resources = attributes.select { |k, v| v.kind_of?(ActiveRecord::Base) }
26
+ return attributes if resources.blank?
27
+
28
+ retval = attributes.except(*resources.keys)
29
+
30
+ resources.each do |k, resource|
31
+ retval["#{k}_id".to_sym] = resource.id
32
+ retval["#{k}_type".to_sym] = resource.class.name
33
+ end
34
+
35
+ retval
36
+ end
37
+
18
38
  end
19
39
  end
20
40
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Effective
2
4
  module EffectiveDatatable
3
5
  module Collection
@@ -34,7 +36,7 @@ module Effective
34
36
  raise 'No collection defined. Please add a collection with collection do ... end' if collection.nil?
35
37
 
36
38
  @collection_class = (collection.respond_to?(:klass) ? collection.klass : self.class)
37
-
39
+
38
40
  @active_record_collection = (collection.ancestors.include?(ActiveRecord::Base) rescue false)
39
41
  @active_record_array_collection = collection.kind_of?(Array) && collection.present? && collection.first.kind_of?(ActiveRecord::Base)
40
42
  @array_collection = collection.kind_of?(Array) && (collection.blank? || collection.first.kind_of?(Array))
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Effective
2
4
  module EffectiveDatatable
3
5
  module Compute
@@ -58,13 +60,14 @@ module Effective
58
60
  finalize(col)
59
61
  end
60
62
 
61
- def arrayize(collection)
63
+ def arrayize(collection, csv: false)
62
64
  collection.map do |obj|
63
65
  columns.map do |name, opts|
64
- if state[:visible][name] == false && (name != order_name) # Sort by invisible array column
66
+ if state[:visible][name] == false && !csv && (name != order_name) # Sort by invisible array column
67
+ BLANK
68
+ elsif csv && !opts[:csv]
65
69
  BLANK
66
70
  elsif opts[:compute]
67
-
68
71
  if array_collection?
69
72
  dsl_tool.instance_exec(obj, obj[opts[:index]], &opts[:compute])
70
73
  else
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Effective
2
4
  module EffectiveDatatable
3
5
  module Cookie
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'csv'
4
+
5
+ module Effective
6
+ module EffectiveDatatable
7
+ module Csv
8
+
9
+ def csv_filename
10
+ self.class.name.underscore.parameterize + '.csv'
11
+ end
12
+
13
+ def csv_content_type
14
+ 'text/csv; charset=utf-8'
15
+ end
16
+
17
+ def csv_header
18
+ columns.map { |_, opts| opts[:label] || '' }
19
+ end
20
+
21
+ def csv_file
22
+ CSV.generate do |csv|
23
+ csv << csv_header()
24
+
25
+ collection.send(csv_collection_method) do |resources|
26
+ resources = arrayize(resources, csv: true)
27
+ format(resources, csv: true)
28
+ finalize(resources)
29
+
30
+ resources.each { |resource| csv << resource }
31
+ end
32
+ end
33
+ end
34
+
35
+ def csv_stream
36
+ EffectiveResources.with_resource_enumerator do |lines|
37
+ lines << CSV.generate_line(csv_header)
38
+
39
+ collection.public_send(csv_collection_method) do |resources|
40
+ resources = arrayize(resources, csv: true)
41
+ format(resources, csv: true)
42
+ finalize(resources)
43
+
44
+ resources.each { |resource| lines << CSV.generate_line(resource) }
45
+ end
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def csv_collection_method
52
+ (active_record_collection? ? :find_in_batches : :to_a)
53
+ end
54
+
55
+ end
56
+ end
57
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Effective
2
4
  module EffectiveDatatable
3
5
  module Dsl
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Effective
2
4
  module EffectiveDatatable
3
5
  module Dsl
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Effective
2
4
  module EffectiveDatatable
3
5
  module Dsl
@@ -24,11 +26,15 @@ module Effective
24
26
  reorder_col(name)
25
27
  end
26
28
 
29
+ def download(bool)
30
+ datatable.attributes[:downloadable] = bool
31
+ end
32
+
27
33
  # A col has its internal values sorted/searched before the block is run
28
34
  # Anything done in the block, is purely a format on the after sorted/ordered value
29
35
  # the original object == the computed value, which is yielded to the format block
30
36
  # 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)
37
+ 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
38
  raise 'You cannot use partial: ... with the block syntax' if partial && block_given?
33
39
 
34
40
  name = name.to_sym unless name.to_s.include?('.')
@@ -38,6 +44,7 @@ module Effective
38
44
  as: as,
39
45
  compute: nil,
40
46
  col_class: col_class,
47
+ csv: csv,
41
48
  format: (format if block_given?),
42
49
  index: nil,
43
50
  label: (label.nil? ? name.to_s.split('.').last.titleize : label),
@@ -56,7 +63,7 @@ module Effective
56
63
 
57
64
  # A val is a computed value that is then sorted/searched after the block is run
58
65
  # 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)
66
+ 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
67
  raise 'You cannot use partial: ... with the block syntax' if partial && block_given?
61
68
 
62
69
  name = name.to_sym unless name.to_s.include?('.')
@@ -66,6 +73,7 @@ module Effective
66
73
  as: as,
67
74
  compute: (compute if block_given?),
68
75
  col_class: col_class,
76
+ csv: csv,
69
77
  format: nil,
70
78
  index: nil,
71
79
  label: (label.nil? ? name.to_s.split('.').last.titleize : label),
@@ -91,6 +99,7 @@ module Effective
91
99
  compute: nil,
92
100
  btn_class: (btn_class || 'btn-sm btn-outline-primary'),
93
101
  col_class: col_class,
102
+ csv: false,
94
103
  format: (format if block_given?),
95
104
  index: nil,
96
105
  inline: (inline.nil? ? datatable.inline? : inline),
@@ -130,6 +139,7 @@ module Effective
130
139
  as: :bulk_actions,
131
140
  compute: nil,
132
141
  col_class: col_class,
142
+ csv: false,
133
143
  format: nil,
134
144
  index: nil,
135
145
  input_name: (input_name || 'bulk_actions_resources'),
@@ -157,6 +167,7 @@ module Effective
157
167
  as: :reorder,
158
168
  compute: nil,
159
169
  col_class: col_class,
170
+ csv: false,
160
171
  format: nil,
161
172
  index: nil,
162
173
  label: false,
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Effective
2
4
  module EffectiveDatatable
3
5
  module Dsl
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Effective
2
4
  module EffectiveDatatable
3
5
  module Dsl
@@ -11,11 +13,11 @@ module Effective
11
13
  end
12
14
 
13
15
  def collection(apply_belongs_to: true, apply_scope: true, &block)
14
- define_method('initialize_collection') {
16
+ define_method('initialize_collection') {
15
17
  self._collection_apply_belongs_to = apply_belongs_to
16
18
  self._collection_apply_scope = apply_scope
17
-
18
- self._collection = dsl_tool.instance_exec(&block)
19
+
20
+ self._collection = dsl_tool.instance_exec(&block)
19
21
  }
20
22
  end
21
23
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Effective
2
4
  module EffectiveDatatable
3
5
  module Format
@@ -8,14 +10,16 @@ module Effective
8
10
 
9
11
  private
10
12
 
11
- def format(collection)
13
+ def format(collection, csv: false)
12
14
  # We want to use the render :collection for each column that renders partials
13
15
  rendered = {}
14
16
 
15
17
  columns.each do |name, opts|
16
- next unless state[:visible][name]
17
-
18
- if opts[:partial]
18
+ if state[:visible][name] == false && !csv
19
+ # Nothing to do
20
+ elsif csv && !opts[:csv]
21
+ # Nothing to do
22
+ elsif opts[:partial]
19
23
  locals = { datatable: self, column: opts }.merge!(resource_col_locals(opts))
20
24
 
21
25
  rendered[name] = (view.render(
@@ -26,6 +30,7 @@ module Effective
26
30
  locals: locals,
27
31
  spacer_template: SPACER_TEMPLATE
28
32
  ) || '').split(SPACER)
33
+
29
34
  elsif opts[:as] == :actions # This is actions_col and actions_col do .. end, but not actions_col partial: 'something'
30
35
  locals = { datatable: self, column: opts, spacer_template: SPACER_TEMPLATE }
31
36
 
@@ -45,7 +50,6 @@ module Effective
45
50
  else
46
51
  (view.render_resource_actions(collection.map { |row| row[opts[:index]] }, atts, &opts[:format]) || '').split(SPACER)
47
52
  end
48
-
49
53
  end
50
54
  end
51
55
 
@@ -54,9 +58,11 @@ module Effective
54
58
  index = opts[:index]
55
59
  value = row[index]
56
60
 
57
- row[index] = (
58
- if state[:visible][name] == false
61
+ formatted = (
62
+ if state[:visible][name] == false && !csv
59
63
  NONVISIBLE
64
+ elsif csv && !opts[:csv]
65
+ BLANK
60
66
  elsif opts[:as] == :actions
61
67
  rendered[name][row_index]
62
68
  elsif opts[:format] && rendered.key?(name)
@@ -66,20 +72,33 @@ module Effective
66
72
  elsif opts[:partial]
67
73
  rendered[name][row_index]
68
74
  else
69
- format_column(value, opts)
75
+ format_column(value, opts, csv: csv)
70
76
  end
71
77
  )
78
+
79
+ if csv && (opts[:format] || opts[:partial])
80
+ formatted = view.strip_tags(formatted)
81
+
82
+ formatted.gsub!("\n\n", ' ') unless formatted.frozen?
83
+ formatted.gsub!("\n", '') unless formatted.frozen?
84
+ end
85
+
86
+ row[index] = formatted
72
87
  end
73
88
  end
74
89
  end
75
90
 
76
- def format_column(value, column)
91
+ def format_column(value, column, csv: false)
77
92
  return if value.nil? || (column[:resource] && value.blank?)
78
93
 
79
94
  unless column[:as] == :email
80
95
  return value if value.kind_of?(String)
81
96
  end
82
97
 
98
+ if value.kind_of?(Array) && column[:as] == :string
99
+ return value.map { |v| view.content_tag(:div, format_column(v, column, csv: csv), class: 'col-resource_item') }.join.html_safe
100
+ end
101
+
83
102
  case column[:as]
84
103
  when :actions
85
104
  raise("please use actions_col instead of col(#{name}, as: :actions)")
@@ -90,7 +109,11 @@ module Effective
90
109
  when :date
91
110
  value.respond_to?(:strftime) ? value.strftime(EffectiveDatatables.format_date) : BLANK
92
111
  when :datetime
93
- value.respond_to?(:strftime) ? value.strftime(EffectiveDatatables.format_datetime) : BLANK
112
+ if csv
113
+ value
114
+ else
115
+ value.respond_to?(:strftime) ? value.strftime(EffectiveDatatables.format_datetime) : BLANK
116
+ end
94
117
  when :decimal
95
118
  value
96
119
  when :duration
@@ -102,7 +125,7 @@ module Effective
102
125
  when :effective_roles
103
126
  value.join(', ')
104
127
  when :email
105
- view.mail_to(value)
128
+ csv ? value : view.mail_to(value)
106
129
  when :integer
107
130
  value
108
131
  when :percent
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Effective
2
4
  module EffectiveDatatable
3
5
  module Hooks
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Effective
2
4
  module EffectiveDatatable
3
5
  module Params
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Effective
2
4
  module EffectiveDatatable
3
5
  module Resource
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Effective
2
4
  module EffectiveDatatable
3
5
  module State
@@ -98,7 +100,7 @@ module Effective
98
100
  load_cookie_state! if cookie.present? && cookie[:params] == cookie_state_params
99
101
  load_filter_params!
100
102
  end
101
-
103
+
102
104
  fill_empty_filters!
103
105
  end
104
106
 
@@ -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.4'.freeze
2
+ VERSION = '4.10.3'.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.4
4
+ version: 4.10.3
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-09-23 00:00:00.000000000 Z
11
+ date: 2021-12-28 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