effective_datatables 4.7.16 → 4.15.1

Sign up to get free protection for your applications and to get access to all the features.
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)