effective_datatables 4.9.4 → 4.10.3

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.
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