effective_datatables 4.7.16 → 4.15.1

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +278 -24
  4. data/app/assets/javascripts/effective_datatables/bulk_actions.js.coffee +32 -9
  5. data/app/assets/javascripts/effective_datatables/download.js.coffee +10 -0
  6. data/app/assets/javascripts/effective_datatables/flash.js.coffee +1 -1
  7. data/app/assets/javascripts/effective_datatables/initialize.js.coffee +22 -13
  8. data/app/assets/javascripts/effective_datatables/inline_crud.js.coffee +42 -13
  9. data/app/assets/javascripts/effective_datatables/reorder.js.coffee +8 -2
  10. data/app/assets/javascripts/effective_datatables/reset.js.coffee +23 -2
  11. data/app/assets/javascripts/vendor/jquery.delayedChange.js +1 -2
  12. data/app/assets/stylesheets/dataTables/dataTables.bootstrap4.scss +1 -3
  13. data/app/controllers/effective/datatables_controller.rb +34 -0
  14. data/app/helpers/effective_datatables_controller_helper.rb +2 -0
  15. data/app/helpers/effective_datatables_helper.rb +22 -6
  16. data/app/helpers/effective_datatables_private_helper.rb +30 -20
  17. data/app/models/effective/datatable.rb +57 -4
  18. data/app/models/effective/datatable_column.rb +2 -0
  19. data/app/models/effective/datatable_column_tool.rb +5 -3
  20. data/app/models/effective/datatable_dsl_tool.rb +7 -5
  21. data/app/models/effective/datatable_value_tool.rb +9 -8
  22. data/app/models/effective/effective_datatable/attributes.rb +21 -0
  23. data/app/models/effective/effective_datatable/collection.rb +3 -1
  24. data/app/models/effective/effective_datatable/compute.rb +11 -7
  25. data/app/models/effective/effective_datatable/cookie.rb +6 -0
  26. data/app/models/effective/effective_datatable/csv.rb +71 -0
  27. data/app/models/effective/effective_datatable/dsl/bulk_actions.rb +3 -1
  28. data/app/models/effective/effective_datatable/dsl/charts.rb +2 -0
  29. data/app/models/effective/effective_datatable/dsl/datatable.rb +20 -6
  30. data/app/models/effective/effective_datatable/dsl/filters.rb +3 -1
  31. data/app/models/effective/effective_datatable/dsl.rb +7 -3
  32. data/app/models/effective/effective_datatable/format.rb +52 -24
  33. data/app/models/effective/effective_datatable/hooks.rb +2 -0
  34. data/app/models/effective/effective_datatable/params.rb +9 -2
  35. data/app/models/effective/effective_datatable/resource.rb +26 -13
  36. data/app/models/effective/effective_datatable/state.rb +4 -2
  37. data/app/views/effective/datatables/_active_storage_column.html.haml +4 -0
  38. data/app/views/effective/datatables/_bulk_actions_dropdown.html.haml +3 -2
  39. data/app/views/effective/datatables/_buttons.html.haml +14 -0
  40. data/config/effective_datatables.rb +8 -1
  41. data/config/locales/en.yml +4 -1
  42. data/config/locales/es.yml +4 -1
  43. data/config/locales/nl.yml +4 -1
  44. data/config/routes.rb +1 -0
  45. data/lib/effective_datatables/engine.rb +6 -4
  46. data/lib/effective_datatables/version.rb +1 -1
  47. data/lib/effective_datatables.rb +5 -0
  48. metadata +11 -10
  49. data/app/datatables/effective_style_guide_datatable.rb +0 -47
  50. data/app/models/effective/access_denied.rb +0 -17
  51. data/app/views/effective/style_guide/_effective_datatables.html.haml +0 -1
@@ -5,6 +5,9 @@ initializeDataTables = (target) ->
5
5
  buttons_export_columns = options['buttons_export_columns'] || ':not(.col-actions)'
6
6
  reorder = datatable.data('reorder')
7
7
 
8
+ if datatable.data('inline') && datatable.closest('form').length > 0
9
+ console.error('inline datatable cannot work inside a form')
10
+
8
11
  if options['buttons'] == false
9
12
  options['buttons'] = []
10
13
 
@@ -49,7 +52,7 @@ initializeDataTables = (target) ->
49
52
  displayStart: datatable.data('display-start')
50
53
  iDisplayLength: datatable.data('display-length')
51
54
  language: datatable.data('language')
52
- lengthMenu: [[5, 10, 25, 50, 100, 250, 500, 9999999], ['5', '10', '25', '50', '100', '250', '500', 'All']]
55
+ lengthMenu: [[5, 10, 25, 50, 100, 250, 500, 9999999], ['5', '10', '25', '50', '100', '250', '500', datatable.data('all-label')]]
53
56
  order: datatable.data('display-order')
54
57
  processing: true
55
58
  responsive: true
@@ -77,7 +80,7 @@ initializeDataTables = (target) ->
77
80
  filter_name = name.replace('filters[', '').substring(0, name.length-9)
78
81
 
79
82
  params['filter'][filter_name] = $form.find("input[name='#{name}']:checked").val()
80
-
83
+
81
84
  else if $input.attr('id')
82
85
  filter_name = $input.attr('id').replace('filters_', '')
83
86
  params['filter'][filter_name] = $input.val()
@@ -109,14 +112,8 @@ initializeDataTables = (target) ->
109
112
  $table = $(api.table().node())
110
113
  $buttons = $table.closest('.dataTables_wrapper').children().first().find('.dt-buttons')
111
114
 
112
- if $table.data('reset')
113
- $buttons.prepend($table.data('reset'))
114
-
115
- if $table.data('reorder')
116
- $buttons.prepend($table.data('reorder'))
117
-
118
- if $table.data('bulk-actions')
119
- $buttons.prepend($table.data('bulk-actions'))
115
+ if $table.data('buttons-html')
116
+ $buttons.prepend($table.data('buttons-html'))
120
117
 
121
118
  drawAggregates = ($table, aggregates) ->
122
119
  $tfoot = $table.find('tfoot').first()
@@ -131,7 +128,7 @@ initializeDataTables = (target) ->
131
128
  if typeof(google) != 'undefined' && typeof(google.visualization) != 'undefined'
132
129
  $.each charts, (name, data) =>
133
130
  $(".effective-datatables-chart[data-name='#{name}']").each (_, obj) =>
134
- chart = new google.visualization[data['type']](obj)
131
+ chart = new google.visualization[data['as']](obj)
135
132
  chart.draw(google.visualization.arrayToDataTable(data['data']), data['options'])
136
133
 
137
134
  # Appends the search html, stored in the column definitions, into each column header
@@ -162,14 +159,23 @@ initializeDataTables = (target) ->
162
159
  $input.on 'change', (event) -> dataTableSearch($(event.currentTarget))
163
160
  else if $input.is('input')
164
161
  $input.delayedChange ($input) -> dataTableSearch($input)
165
- $input.on('paste', -> dataTableSearch($input))
166
162
 
167
163
  # Do the actual search
168
164
  dataTableSearch = ($input) -> # This is the function called by a select or input to run the search
169
165
  return if $input.is(':invalid')
170
166
 
171
167
  table = $input.closest('table.dataTable')
172
- table.DataTable().column("#{$input.data('column-name')}:name").search($input.val()).draw()
168
+
169
+ value = $input.val()
170
+
171
+ if Array.isArray(value)
172
+ # Nothing
173
+ else if value.startsWith('"') && value.endsWith('"')
174
+ value = value.substring(1, value.length-1)
175
+ else
176
+ value = $.trim(value)
177
+
178
+ table.DataTable().column("#{$input.data('column-name')}:name").search(value).draw()
173
179
 
174
180
  if reorder
175
181
  init_options['rowReorder'] = { selector: 'td.col-_reorder', snapX: true, dataSrc: datatable.data('reorder-index') }
@@ -188,6 +194,7 @@ initializeDataTables = (target) ->
188
194
 
189
195
  table.addClass('initialized')
190
196
  table.children('thead').trigger('effective-bootstrap:initialize')
197
+ table.children('thead').find('input[autofocus]').first().focus()
191
198
  true
192
199
 
193
200
  destroyDataTables = ->
@@ -201,3 +208,5 @@ $(document).on 'page:change', -> initializeDataTables()
201
208
  $(document).on 'turbolinks:load', -> initializeDataTables()
202
209
  $(document).on 'turbolinks:render', -> initializeDataTables()
203
210
  $(document).on 'turbolinks:before-cache', -> destroyDataTables()
211
+ $(document).on 'turbo:load', -> initializeDataTables()
212
+ $(document).on 'turbo:before-cache', -> destroyDataTables()
@@ -1,15 +1,34 @@
1
1
  # To achieve inline crud, we use rails' data-remote links, and override their behaviour when inside a datatable
2
2
  # This works with EffectiveForm.remote_form which is part of the effective_bootstrap gem.
3
3
 
4
- # We click the New/Edit/Action button from the col-actions
5
- $(document).on 'ajax:beforeSend', '.dataTables_wrapper .col-actions', (e, xhr, settings) ->
6
- $action = $(e.target)
7
- $table = $(e.target).closest('table')
4
+ # https://github.com/rails/jquery-ujs/wiki/ajax
5
+ # https://edgeguides.rubyonrails.org/working_with_javascript_in_rails.html#rails-ujs-event-handlers
6
+
7
+ $(document).on 'ajax:before', '.dataTables_wrapper .col-actions', (event) ->
8
+ $action = $(event.target)
9
+ $table = $(event.target).closest('table')
8
10
 
9
11
  return true if ('' + $action.data('inline')) == 'false'
10
12
 
11
- $params = $.param({_datatable_id: $table.attr('id'), _datatable_attributes: $table.data('attributes'), _datatable_action: true })
12
- settings.url += (if settings.url.indexOf('?') == -1 then '?' else '&') + $params
13
+ $params = $.param(
14
+ {
15
+ _datatable_id: $table.attr('id'),
16
+ _datatable_attributes: $table.data('attributes'),
17
+ _datatable_action: true
18
+ }
19
+ )
20
+
21
+ $action.attr('data-params', $params)
22
+ true
23
+
24
+ # We click the New/Edit/Action button from the col-actions
25
+ $(document).on 'ajax:beforeSend', '.dataTables_wrapper .col-actions', (event, xhr, settings) ->
26
+ [xhr, settings] = event.detail if event.detail # rails/ujs
27
+
28
+ $action = $(event.target)
29
+ $table = $(event.target).closest('table')
30
+
31
+ return true if ('' + $action.data('inline')) == 'false'
13
32
 
14
33
  if $action.closest('.effective-datatables-inline-row,table.dataTable').hasClass('effective-datatables-inline-row')
15
34
  # Nothing.
@@ -22,6 +41,8 @@ $(document).on 'ajax:beforeSend', '.dataTables_wrapper .col-actions', (e, xhr, s
22
41
 
23
42
  # We have either completed the resource action, or fetched the inline form to load.
24
43
  $(document).on 'ajax:success', '.dataTables_wrapper .col-actions', (event, data) ->
44
+ [data, status, xhr] = event.detail if event.detail # rails/ujs
45
+
25
46
  $action = $(event.target)
26
47
 
27
48
  return true if ('' + $action.data('inline')) == 'false'
@@ -54,12 +75,22 @@ $(document).on 'ajax:error', '.dataTables_wrapper', (event) ->
54
75
  EffectiveForm.remote_form_flash = ''
55
76
  true
56
77
 
57
- # Submitting an inline datatables form
58
- $(document).on 'ajax:beforeSend', '.dataTables_wrapper .col-inline-form', (e, xhr, settings) ->
59
- $table = $(e.target).closest('table')
78
+ ## Now for the fetched form. We add the datatables params attributes
79
+
80
+ $(document).on 'ajax:before', '.dataTables_wrapper .col-inline-form', (event) ->
81
+ $action = $(event.target)
82
+ $form = $action.closest('form')
83
+ $table = $action.closest('table')
84
+
85
+ if $form.find('input[name=_datatable_id]').length == 0
86
+ $('<input>').attr(
87
+ {type: 'hidden', name: '_datatable_id', value: $table.attr('id')}
88
+ ).appendTo($form)
60
89
 
61
- $params = $.param({_datatable_id: $table.attr('id'), _datatable_attributes: $table.data('attributes') })
62
- settings.url += (if settings.url.indexOf('?') == -1 then '?' else '&') + $params
90
+ if $form.find('input[name=_datatable_attributes]').length == 0
91
+ $('<input>').attr(
92
+ {type: 'hidden', name: '_datatable_attributes', value: $table.data('attributes')}
93
+ ).appendTo($form)
63
94
 
64
95
  true
65
96
 
@@ -92,7 +123,6 @@ beforeNew = ($action) ->
92
123
 
93
124
  # Append spinner and show Processing
94
125
  $th.append($table.data('spinner'))
95
- $table.DataTable().flash()
96
126
  $table.one 'draw.dt', (event) ->
97
127
  $th.find('a').show().siblings('svg').remove() if event.target == event.currentTarget
98
128
 
@@ -121,7 +151,6 @@ beforeEdit = ($action) ->
121
151
 
122
152
  # Append spinner and show Processing
123
153
  $td.append($table.data('spinner'))
124
- $table.DataTable().flash()
125
154
 
126
155
  afterEdit = ($action) ->
127
156
  $tr = $action.closest('tr')
@@ -8,7 +8,14 @@ reorder = (event, diff, edit) ->
8
8
  return unless oldNode? && newNode?
9
9
 
10
10
  url = @context[0].ajax.url.replace('.json', '/reorder.json')
11
- data = {'reorder[id]': oldNode.data('reorder-resource'), 'reorder[old]': oldNode.val(), 'reorder[new]': newNode.val(), attributes: $table.data('attributes') }
11
+
12
+ data = {
13
+ 'authenticity_token': $('head').find("meta[name='csrf-token']").attr('content'),
14
+ 'reorder[id]': oldNode.data('reorder-resource'),
15
+ 'reorder[old]': oldNode.val(),
16
+ 'reorder[new]': newNode.val(),
17
+ 'attributes': $table.data('attributes')
18
+ }
12
19
 
13
20
  @context[0].rowreorder.c.enable = false
14
21
 
@@ -40,4 +47,3 @@ $(document).on 'click', '.dataTables_wrapper a.buttons-reorder', (event) ->
40
47
  $table.addClass('reordering')
41
48
 
42
49
  column.visible(!column.visible())
43
-
@@ -1,11 +1,32 @@
1
1
  $(document).on 'click', '.dataTables_wrapper a.buttons-reset-search', (event) ->
2
2
  event.preventDefault() # prevent the click
3
3
 
4
+ # Reset the HTML
4
5
  $table = $(event.currentTarget).closest('.dataTables_wrapper').find('table.dataTable').first()
5
6
  $thead = $table.children('thead').first()
6
7
 
7
- $thead.find('input').val('').removeAttr('checked').removeAttr('selected')
8
+ # Reset all inputs
8
9
  $thead.find('select').val('').trigger('change.select2')
9
10
 
10
- $table.DataTable().search('').columns().search('').draw()
11
+ $inputs = $thead.find('input')
12
+ $inputs.val('').removeAttr('checked').removeAttr('selected')
13
+
14
+ # Reset delayedChange
15
+ $.each $inputs, (input) =>
16
+ $input = $(input)
17
+ if ($input.delayedChange.oldVal)
18
+ $input.delayedChange.oldVal = undefined
19
+
20
+ # Reset the datatable
21
+ datatable = $table.DataTable()
22
+
23
+ # Reset search
24
+ datatable.search('').columns().search('')
25
+
26
+ # Reset to default visibility
27
+ $.each $table.data('default-visibility'), (index, visible) =>
28
+ datatable.column(index).visible(visible, false)
29
+
30
+ # Don't pass up the click
31
+ false
11
32
 
@@ -14,11 +14,10 @@
14
14
 
15
15
  return this.each(function() {
16
16
  var element = $(this);
17
- element.keyup(function() {
17
+ element.on('keyup paste', function() {
18
18
  clearTimeout(timer);
19
19
  timer = setTimeout(function() {
20
20
  var newVal = element.val();
21
- newVal = $.trim(newVal);
22
21
  if (element.delayedChange.oldVal != newVal) {
23
22
  element.delayedChange.oldVal = newVal;
24
23
  o.onChange.call(this, element);
@@ -162,9 +162,7 @@ div.dataTables_scrollFoot > .dataTables_scrollFootInner > table {
162
162
  text-align: center;
163
163
  }
164
164
  }
165
- table.dataTable.table-sm > thead > tr > th {
166
- padding-right: 20px;
167
- }
165
+
168
166
  table.dataTable.table-sm .sorting:before,
169
167
  table.dataTable.table-sm .sorting_asc:before,
170
168
  table.dataTable.table-sm .sorting_desc:before {
@@ -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,18 +1,29 @@
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, 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
21
+ datatable.attributes[:namespace] = namespace if namespace
13
22
 
14
23
  datatable.view ||= self
15
24
 
25
+ datatable.state[:length] = 9999999 if simple
26
+
16
27
  unless EffectiveDatatables.authorized?(controller, :index, datatable.collection_class)
17
28
  return content_tag(:p, "You are not authorized to view this datatable. (cannot :index, #{datatable.collection_class})")
18
29
  end
@@ -40,10 +51,12 @@ module EffectiveDatatablesHelper
40
51
  id: datatable.to_param,
41
52
  class: html_class,
42
53
  data: {
54
+ 'all-label' => I18n.t('effective_datatables.all'),
43
55
  'attributes' => EffectiveDatatables.encrypt(datatable.attributes),
44
56
  'authenticity-token' => form_authenticity_token,
45
- 'bulk-actions' => datatable_bulk_actions(datatable),
57
+ 'buttons-html' => datatable_buttons(datatable),
46
58
  'columns' => datatable_columns(datatable),
59
+ 'default-visibility' => datatable.default_visibility.to_json,
47
60
  'display-length' => datatable.display_length,
48
61
  'display-order' => datatable_display_order(datatable),
49
62
  'display-records' => datatable.to_json[:recordsFiltered],
@@ -51,8 +64,7 @@ module EffectiveDatatablesHelper
51
64
  'inline' => inline.to_s,
52
65
  'language' => EffectiveDatatables.language(I18n.locale),
53
66
  'options' => input_js.to_json,
54
- 'reset' => (datatable_reset(datatable) if search),
55
- 'reorder' => datatable_reorder(datatable),
67
+ 'reorder' => datatable.reorder?.to_s,
56
68
  'reorder-index' => (datatable.columns[:_reorder][:index] if datatable.reorder?).to_s,
57
69
  'simple' => simple.to_s,
58
70
  'spinner' => icon('spinner'), # effective_bootstrap
@@ -61,7 +73,7 @@ module EffectiveDatatablesHelper
61
73
  }
62
74
  }
63
75
 
64
- if (charts || filters)
76
+ retval = if (charts || filters)
65
77
  output = ''.html_safe
66
78
 
67
79
  if charts
@@ -82,6 +94,10 @@ module EffectiveDatatablesHelper
82
94
  locals: { datatable: datatable, effective_datatable_params: effective_datatable_params }
83
95
  )
84
96
  end
97
+
98
+ Rails.logger.info(" Rendered datatable #{datatable.class} #{datatable.source_location}")
99
+
100
+ retval
85
101
  end
86
102
 
87
103
  def render_inline_datatable(datatable)
@@ -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,32 +21,21 @@ 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)
42
- return unless column[:inline] && (column[:actions][:new] != false)
33
+ return unless datatable.inline? && (column[:actions][:new] != false)
43
34
 
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 = 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)
@@ -68,8 +59,23 @@ module EffectiveDatatablesPrivateHelper
68
59
  when :reorder
69
60
  content_tag(:span, t('effective_datatables.reorder'), style: 'display: none;')
70
61
  else
71
- content_tag(:span, opts[:label].presence)
62
+ label = opts[:label].presence || datatable_human_attribute_name(datatable, name, opts)
63
+ content_tag(:span, label)
64
+ end
65
+ end
66
+
67
+ def datatable_human_attribute_name(datatable, name, opts)
68
+ return (name.to_s.split('.').last || '').titleize unless datatable.active_record_collection?
69
+
70
+ case opts[:as]
71
+ when :belongs_to
72
+ opts[:resource].human_name
73
+ when :has_many
74
+ opts[:resource].human_plural_name
75
+ else
76
+ datatable.collection_class.human_attribute_name(name)
72
77
  end
78
+
73
79
  end
74
80
 
75
81
  def datatable_search_tag(datatable, name, opts)
@@ -78,13 +84,13 @@ module EffectiveDatatablesPrivateHelper
78
84
  return if opts[:search] == false
79
85
 
80
86
  # Build the search
81
- @_effective_datatables_form_builder || effective_form_with(scope: :datatable_search, url: '#') { |f| @_effective_datatables_form_builder = f }
87
+ @_effective_datatables_form_builder || effective_form_with(scope: 'datatable_search', url: '#') { |f| @_effective_datatables_form_builder = f }
82
88
  form = @_effective_datatables_form_builder
83
89
 
84
90
  collection = opts[:search].delete(:collection)
85
91
  value = datatable.state[:search][name]
86
92
 
87
- options = opts[:search].except(:fuzzy).merge!(
93
+ options = opts[:search].merge(
88
94
  name: nil,
89
95
  feedback: false,
90
96
  label: false,
@@ -92,6 +98,8 @@ module EffectiveDatatablesPrivateHelper
92
98
  data: { 'column-name': name, 'column-index': opts[:index] }
93
99
  )
94
100
 
101
+ options.delete(:fuzzy)
102
+
95
103
  case options.delete(:as)
96
104
  when :string, :text, :number
97
105
  form.text_field name, options
@@ -139,7 +147,7 @@ module EffectiveDatatablesPrivateHelper
139
147
  placeholder: (opts[:label] || name.to_s.titleize),
140
148
  value: value,
141
149
  wrapper: { class: 'form-group col-auto'}
142
- }.merge(opts.except(:as, :collection, :parse))
150
+ }.merge(opts.except(:as, :collection, :parse, :value))
143
151
 
144
152
  options[:name] = '' unless datatable._filters_form_required?
145
153
 
@@ -149,6 +157,8 @@ module EffectiveDatatablesPrivateHelper
149
157
  elsif as == :boolean
150
158
  collection ||= [true, false].map { |value| [t("effective_datatables.boolean_#{value}"), value] }
151
159
  form.public_send(:select, name, collection, options) # boolean
160
+ elsif as == :string
161
+ form.public_send(:text_field, name, options)
152
162
  elsif form.respond_to?(as)
153
163
  form.public_send(as, name, options) # check_box, text_area
154
164
  else
@@ -169,7 +179,7 @@ module EffectiveDatatablesPrivateHelper
169
179
  label: false,
170
180
  required: false,
171
181
  wrapper: { class: 'form-group col-auto'}
172
- }.merge(opts)
182
+ }.merge(opts.except(:checked, :value))
173
183
 
174
184
  form.radios :scope, collection, options
175
185
  end
@@ -1,8 +1,10 @@
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.
4
- attr_reader :effective_resource
5
6
  attr_reader :state
7
+ attr_accessor :effective_resource
6
8
 
7
9
  # Hashes of DSL options
8
10
  attr_reader :_aggregates
@@ -21,22 +23,26 @@ module Effective
21
23
  # The view
22
24
  attr_reader :view
23
25
 
26
+ # Set by DSL so we can track where this datatable is coming from
27
+ attr_accessor :source_location
28
+
24
29
  extend Effective::EffectiveDatatable::Dsl
25
30
 
26
31
  include Effective::EffectiveDatatable::Attributes
27
32
  include Effective::EffectiveDatatable::Collection
28
33
  include Effective::EffectiveDatatable::Compute
29
34
  include Effective::EffectiveDatatable::Cookie
35
+ include Effective::EffectiveDatatable::Csv
30
36
  include Effective::EffectiveDatatable::Format
31
37
  include Effective::EffectiveDatatable::Hooks
32
38
  include Effective::EffectiveDatatable::Params
33
39
  include Effective::EffectiveDatatable::Resource
34
40
  include Effective::EffectiveDatatable::State
35
41
 
36
- def initialize(view = nil, attributes = nil)
42
+ def initialize(view = nil, attributes = nil)
37
43
  (attributes = view; view = nil) if view.kind_of?(Hash)
38
44
 
39
- @attributes = (attributes || {})
45
+ @attributes = initial_attributes(attributes)
40
46
  @state = initial_state
41
47
 
42
48
  @_aggregates = {}
@@ -49,13 +55,40 @@ module Effective
49
55
 
50
56
  raise 'expected a hash of arguments' unless @attributes.kind_of?(Hash)
51
57
  raise 'collection is defined as a method. Please use the collection do ... end syntax.' unless collection.nil?
58
+
52
59
  self.view = view if view
53
60
  end
54
61
 
62
+ def rendered(params = {})
63
+ raise('expected a hash of params') unless params.kind_of?(Hash)
64
+
65
+ view = ApplicationController.renderer.controller.helpers
66
+
67
+ view.class_eval do
68
+ attr_accessor :rendered_params
69
+
70
+ def current_user
71
+ rendered_params[:current_user]
72
+ end
73
+ end
74
+
75
+ if params[:current_user_id]
76
+ params[:current_user] = User.find(params[:current_user_id])
77
+ end
78
+
79
+ view.rendered_params = params
80
+
81
+ self.view = view
82
+ self
83
+ end
84
+
55
85
  # Once the view is assigned, we initialize everything
56
86
  def view=(view)
57
87
  @view = (view.respond_to?(:view_context) ? view.view_context : view)
58
- raise 'expected view to respond to params' unless @view.respond_to?(:params)
88
+
89
+ unless @view.respond_to?(:params) || @view.respond_to?(:rendered_params)
90
+ raise 'expected view to respond to params'
91
+ end
59
92
 
60
93
  assert_attributes!
61
94
  load_attributes!
@@ -110,6 +143,10 @@ module Effective
110
143
  to_json[:recordsTotal] == 0
111
144
  end
112
145
 
146
+ def to_csv
147
+ csv_file()
148
+ end
149
+
113
150
  def to_json
114
151
  @json ||= (
115
152
  {
@@ -137,6 +174,18 @@ module Effective
137
174
  !reorder? && attributes[:sortable] != false
138
175
  end
139
176
 
177
+ def searchable?
178
+ attributes[:searchable] != false
179
+ end
180
+
181
+ def downloadable?
182
+ attributes[:downloadable] != false
183
+ end
184
+
185
+ def skip_save_state?
186
+ attributes[:skip_save_state] == true
187
+ end
188
+
140
189
  # Whether the filters must be rendered as a <form> or we can keep the normal <div> behaviour
141
190
  def _filters_form_required?
142
191
  _form[:verb].present?
@@ -170,6 +219,10 @@ module Effective
170
219
  @fallback_effective_resource ||= Effective::Resource.new('', namespace: controller_namespace)
171
220
  end
172
221
 
222
+ def default_visibility
223
+ columns.values.inject({}) { |h, col| h[col[:index]] = col[:visible]; h }
224
+ end
225
+
173
226
  private
174
227
 
175
228
  def column_tool
@@ -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
@@ -43,7 +45,7 @@ module Effective
43
45
  Rails.logger.info "COLUMN TOOL: order_column #{column.to_s} #{direction} #{sql_column}" if EffectiveDatatables.debug
44
46
 
45
47
  if column[:sql_as_column]
46
- collection.order("#{sql_column} #{datatable.effective_resource.sql_direction(direction)}")
48
+ collection.order(Arel.sql("#{sql_column} #{datatable.effective_resource.sql_direction(direction)}"))
47
49
  else
48
50
  Effective::Resource.new(collection)
49
51
  .order(column[:name], direction, as: column[:as], sort: column[:sort], sql_column: column[:sql_column], limit: datatable.limit)
@@ -73,10 +75,10 @@ module Effective
73
75
  end
74
76
 
75
77
  def search_column(collection, value, column, sql_column)
76
- Rails.logger.info "COLUMN TOOL: search_column #{column.to_s} #{value} #{sql_column}" if EffectiveDatatables.debug
78
+ Rails.logger.info "COLUMN TOOL: search_column #{column.to_s} value=#{value} operation=#{column[:search][:operation]} column=#{sql_column}" if EffectiveDatatables.debug
77
79
 
78
80
  Effective::Resource.new(collection)
79
- .search(column[:name], value, as: column[:as], fuzzy: column[:search][:fuzzy], sql_column: sql_column)
81
+ .search(column[:name], value, as: column[:as], operation: column[:search][:operation], column: sql_column)
80
82
  end
81
83
 
82
84
  def paginate(collection)