diff --git a/app/assets/javascripts/effective_datatables/initialize.js.coffee b/app/assets/javascripts/effective_datatables/initialize.js.coffee
index 4c7c01e..6e89dcc 100644
--- a/app/assets/javascripts/effective_datatables/initialize.js.coffee
+++ b/app/assets/javascripts/effective_datatables/initialize.js.coffee
@@ -3,6 +3,7 @@ initializeDataTables = (target) ->
datatable = $(this)
options = datatable.data('options') || {}
buttons_export_columns = options['buttons_export_columns'] || ':not(.col-actions)'
+ nested = datatable.data('nested')
reorder = datatable.data('reorder')
if datatable.data('inline') && datatable.closest('form').length > 0
@@ -178,6 +179,9 @@ initializeDataTables = (target) ->
if reorder
table.DataTable().on('row-reorder', (event, diff, edit) -> $(event.target).DataTable().reorder(event, diff, edit))
+ if nested
+ table.closest('.dataTables_wrapper').addClass('dataTables_wrapper_nested')
+
table.addClass('initialized')
table.children('thead').trigger('effective-bootstrap:initialize')
table.children('thead').find('input[autofocus]').first().focus()
diff --git a/app/assets/javascripts/effective_datatables/inline_crud.js.coffee b/app/assets/javascripts/effective_datatables/inline_crud.js.coffee
index 2089f7c..7b438d7 100644
--- a/app/assets/javascripts/effective_datatables/inline_crud.js.coffee
+++ b/app/assets/javascripts/effective_datatables/inline_crud.js.coffee
@@ -9,6 +9,7 @@ $(document).on 'ajax:before', '.dataTables_wrapper .col-actions', (event) ->
$table = $(event.target).closest('table')
return true if ('' + $action.data('inline')) == 'false'
+ return true if ('' + $action.data('nested-datatable')) == 'true'
$params = $.param(
{
@@ -156,7 +157,12 @@ afterEdit = ($action) ->
$tr = $action.closest('tr')
$table = $tr.closest('table')
- html = buildRow($tr.children('td').length, EffectiveForm.remote_form_payload)
+ nestedDatatableAction = $tr.find('a[data-nested-datatable]')
+
+ html = if nestedDatatableAction.length > 0
+ buildNestedDatatableRow($tr.children('td').length, EffectiveForm.remote_form_payload, nestedDatatableAction.attr('title'))
+ else
+ buildRow($tr.children('td').length, EffectiveForm.remote_form_payload)
$tr.data('inline-form-original-html', $tr.children().detach())
$tr.html(html)
@@ -200,6 +206,12 @@ buildRow = (length, payload) ->
"Cancel" +
""
+buildNestedDatatableRow = (length, payload, title) ->
+ "
| " +
+ "" +
+ "Cancel" +
+ " | "
+
expand = ($table) ->
$wrapper = $table.closest('.dataTables_wrapper').addClass('effective-datatables-inline-expanded')
$table.on 'draw.dt', (event) ->
diff --git a/app/assets/javascripts/effective_datatables/nested.js.coffee b/app/assets/javascripts/effective_datatables/nested.js.coffee
new file mode 100644
index 0000000..5cdcb0a
--- /dev/null
+++ b/app/assets/javascripts/effective_datatables/nested.js.coffee
@@ -0,0 +1,5 @@
+# Make all links for nested datatables open in new tabs
+$(document).on 'click', '.dataTables_wrapper_nested a', (event) ->
+ $link = $(event.currentTarget)
+ $link.attr('target', '_blank')
+ true
diff --git a/app/assets/stylesheets/effective_datatables/_overrides.bootstrap4.scss b/app/assets/stylesheets/effective_datatables/_overrides.bootstrap4.scss
index 80c64d0..0682b37 100644
--- a/app/assets/stylesheets/effective_datatables/_overrides.bootstrap4.scss
+++ b/app/assets/stylesheets/effective_datatables/_overrides.bootstrap4.scss
@@ -72,7 +72,7 @@ table.dataTable > thead .sorting_desc {
table.dataTable thead {
th.col-actions {
text-align: right;
- span { display: block; height: 1.25em; }
+ span { display: block; }
}
}
@@ -103,6 +103,15 @@ table.dataTable thead {
}
}
+// When Inline Expanded with Nested
+.dataTables_wrapper.effective-datatables-inline-expanded {
+ .dataTables_wrapper_nested {
+ .dt-buttons { opacity: inherit; }
+ .dataTables_entries { opacity: inherit; }
+ .dataTables_paginate { opacity: inherit; }
+ }
+}
+
table.dataTable.dtr-inline.collapsed > tbody > tr > td.dtr-control:before,
table.dataTable.dtr-inline.collapsed > tbody > tr > th.dtr-control:before {
color: #0275d8;
@@ -175,6 +184,10 @@ table.dataTable > thead {
p { margin-bottom: 0; }
}
+.form-group.datatables-filters-present {
+ margin-top: 1.75em;
+}
+
// Processing div
div.dataTables_wrapper div.dataTables_processing {
margin: -26px 25% 0px 25%;
@@ -312,3 +325,14 @@ div.dt-button-collection .dt-button-active:after {
right: auto;
left: 0.5rem;
}
+
+// Nested Datatables
+.nested-datatable-container-parent {
+ padding-left: 0.25em;
+ padding-right: 0.25em;
+ container-type: inline-size;
+}
+
+.nested-datatable-container {
+ width: 100%;
+}
diff --git a/app/helpers/effective_datatables_helper.rb b/app/helpers/effective_datatables_helper.rb
index 3d61e0a..c5d2732 100644
--- a/app/helpers/effective_datatables_helper.rb
+++ b/app/helpers/effective_datatables_helper.rb
@@ -2,7 +2,7 @@
# These are expected to be called by a developer. They are part of the datatables DSL.
module EffectiveDatatablesHelper
- 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, short: false, sort: true)
+ def render_datatable(datatable, input_js: {}, buttons: true, charts: true, download: nil, entries: true, filters: true, inline: false, namespace: nil, nested: false, pagination: true, search: true, simple: false, short: false, sort: true)
raise 'expected datatable to be present' unless datatable
raise 'expected input_js to be a Hash' unless input_js.kind_of?(Hash)
@@ -19,6 +19,7 @@ def render_datatable(datatable, input_js: {}, buttons: true, charts: true, downl
end
datatable.attributes[:inline] = true if inline
+ datatable.attributes[:nested] = true if nested
datatable.attributes[:sortable] = false unless sort
datatable.attributes[:searchable] = false unless search
datatable.attributes[:downloadable] = false unless download
@@ -68,6 +69,7 @@ def render_datatable(datatable, input_js: {}, buttons: true, charts: true, downl
'inline' => inline.to_s,
'language' => EffectiveDatatables.language(I18n.locale),
'length-menu' => datatable_length_menu(datatable),
+ 'nested' => nested.to_s,
'options' => input_js.to_json,
'reorder' => datatable.reorder?.to_s,
'reorder-index' => (datatable.columns[:_reorder][:index] if datatable.reorder?).to_s,
@@ -133,4 +135,11 @@ def inline_datatable
@_inline_datatable ||= datatable
end
+ def nested_datatable_link_to(title, path, options = {})
+ options[:class] ||= 'btn btn-sm btn-link'
+ options['data-remote'] = true
+ options['data-nested-datatable'] = true
+
+ link_to(title, path, options)
+ end
end
diff --git a/app/helpers/effective_datatables_private_helper.rb b/app/helpers/effective_datatables_private_helper.rb
index 4bacb71..ab94521 100644
--- a/app/helpers/effective_datatables_private_helper.rb
+++ b/app/helpers/effective_datatables_private_helper.rb
@@ -44,7 +44,8 @@ def datatable_length_menu(datatable)
def datatable_new_resource_button(datatable, name, column)
return unless datatable.inline? && (column[:actions][:new] != false)
- action = { action: :new, class: ['btn', column[:btn_class].presence].compact.join(' '), 'data-remote': true }
+ # Override the default btn_class and use this one
+ action = { action: :new, class: 'btn btn-sm btn-success', 'data-remote': true }
if column[:actions][:new].kind_of?(Hash) # This might be active_record_array_collection?
actions = action.merge(column[:actions][:new])
diff --git a/app/models/effective/effective_datatable/dsl.rb b/app/models/effective/effective_datatable/dsl.rb
index e4a574d..cd50fb5 100644
--- a/app/models/effective/effective_datatable/dsl.rb
+++ b/app/models/effective/effective_datatable/dsl.rb
@@ -5,7 +5,10 @@ module EffectiveDatatable
module Dsl
def bulk_actions(&block)
- define_method('initialize_bulk_actions') { dsl_tool.instance_exec(&block); dsl_tool.bulk_actions_col }
+ define_method('initialize_bulk_actions') do
+ actions = dsl_tool.instance_exec(&block)
+ dsl_tool.bulk_actions_col if actions.present?
+ end
end
def charts(&block)
diff --git a/app/models/effective/effective_datatable/format.rb b/app/models/effective/effective_datatable/format.rb
index 88b715d..dd428a2 100644
--- a/app/models/effective/effective_datatable/format.rb
+++ b/app/models/effective/effective_datatable/format.rb
@@ -117,7 +117,14 @@ def format_column(value, column, as: nil, csv: false)
when :actions
raise("please use actions_col instead of col(#{name}, as: :actions)")
when :boolean
- view.t("effective_datatables.boolean_#{value}")
+ label = view.t("effective_datatables.boolean_#{value}")
+
+ if csv
+ label.to_s
+ else
+ color = value ? EffectiveDatatables.format_true : EffectiveDatatables.format_false
+ color.present? ? view.badge(label, color) : label
+ end
when :currency
view.number_to_currency(value)
when :date
diff --git a/app/models/effective/effective_datatable/resource.rb b/app/models/effective/effective_datatable/resource.rb
index b08b6cb..ecfccce 100644
--- a/app/models/effective/effective_datatable/resource.rb
+++ b/app/models/effective/effective_datatable/resource.rb
@@ -160,9 +160,12 @@ def load_resource_search!
columns.each do |name, opts|
# Normalize the given opts[:search] into a Hash
# Take special note of the opts[:search] as we need to collapse it when an ActiveRecord::Relation
- case opts[:search]
- when false
+
+ if opts[:search] == false || attributes[:searchable] == false
opts[:search] = { as: :null }; next
+ end
+
+ case opts[:search]
when Symbol
opts[:search] = { as: opts[:search] }
when Array, ActiveRecord::Relation
@@ -176,10 +179,12 @@ def load_resource_search!
# Now lets deal with the opts[:search] hash itself
search = opts[:search]
+ # Adjust based on shorthand search: :select syntax
+ search[:as] ||= :select if search.key?(:collection)
+ search[:value] ||= search.delete(:selected) if search.key?(:selected)
+
# Parameterize collection
- if attributes[:searchable] == false
- # Nothing to do
- elsif search[:collection].kind_of?(ActiveRecord::Relation)
+ if search[:collection].kind_of?(ActiveRecord::Relation)
search[:collection] = search[:collection].map { |obj| [obj.to_s, obj.id] }
elsif search[:collection].kind_of?(Array) && search[:collection].first.kind_of?(ActiveRecord::Base)
search[:collection] = search[:collection].map { |obj| [obj.to_s, obj.id] }
@@ -187,23 +192,24 @@ def load_resource_search!
search[:collection] = search[:collection]
end
- search[:as] ||= :select if search.key?(:collection)
- search[:value] ||= search.delete(:selected) if search.key?(:selected)
-
# Merge with defaults
search_resource = [opts[:resource], effective_resource, fallback_effective_resource].compact
search_resource = search_resource.find { |res| res.klass.present? } || search_resource.first
- # Assign search collections from effective_resources
- if attributes[:searchable] == false
- # Nothing to do
- elsif search[:as] == :string
+ if search[:as] == :string
# Nothing to do. We're just a string search.
elsif search[:as] == :select && search[:collection].kind_of?(Array)
# Nothing to do. We already loaded the custom parameterized collection above.
- elsif array_collection? && opts[:resource].present?
+ elsif search[:as] == :select && search[:collection].blank? && array_collection? && opts[:resource].present?
# Assigns { as: :select, collection: [...] }
search.reverse_merge!(search_resource.search_form_field(name, collection.first[opts[:index]]))
+ elsif search[:as] == :select && search[:collection].blank?
+ # Load the defaults from effective_resources
+ # Assigns { as: :string } or { as: :select, collection: [...] }
+ search.reverse_merge!(search_resource.search_form_field(name, opts[:as]))
+ elsif [:belongs_to, :belongs_to_polymorphic, :has_and_belongs_to_many, :has_many, :has_one].include?(opts[:as])
+ # Do not eager load the collection. Treat this as a string search.
+ search.reverse_merge!({ as: :string })
else
# Load the defaults from effective_resources
# Assigns { as: :string } or { as: :select, collection: [...] }
diff --git a/app/views/effective/datatables/_filters.html.haml b/app/views/effective/datatables/_filters.html.haml
index 198437c..f4fc230 100644
--- a/app/views/effective/datatables/_filters.html.haml
+++ b/app/views/effective/datatables/_filters.html.haml
@@ -11,7 +11,7 @@
- else
= datatable_filter_tag(form, datatable, name, opts)
- .form-group.col-auto
+ .form-group.col-auto{class: (datatable._filters.present? ? 'datatables-filters-present' : 'datatables-filters-blank')}
- if datatable._filters_form_required?
= form.save t('effective_datatables.apply'), 'data-disable-with': t('effective_datatables.applying'), class: 'btn btn-sm btn-secondary'
- else
diff --git a/config/effective_datatables.rb b/config/effective_datatables.rb
index fab211a..2f51a8d 100644
--- a/config/effective_datatables.rb
+++ b/config/effective_datatables.rb
@@ -46,6 +46,10 @@
config.format_date = '%F'
config.format_time = '%H:%M'
+ # Boolean formatting. When present will render booleans as badges with this bootstrap color
+ config.format_true = 'success'
+ config.format_false = 'danger'
+
# Enable the Download button which serves a CSV of your collection
config.download = false
diff --git a/effective_datatables.gemspec b/effective_datatables.gemspec
index f6e6ebd..b24bd18 100644
--- a/effective_datatables.gemspec
+++ b/effective_datatables.gemspec
@@ -18,6 +18,7 @@ Gem::Specification.new do |s|
s.add_dependency 'rails', '>= 3.2.0'
s.add_dependency 'coffee-rails'
+ s.add_dependency 'csv'
s.add_dependency 'effective_bootstrap'
s.add_dependency 'effective_resources'
s.add_dependency 'sassc'
diff --git a/lib/effective_datatables.rb b/lib/effective_datatables.rb
index 17df712..5e64cc0 100644
--- a/lib/effective_datatables.rb
+++ b/lib/effective_datatables.rb
@@ -22,6 +22,9 @@ module EffectiveDatatables
mattr_accessor :format_date
mattr_accessor :format_time
+ mattr_accessor :format_true
+ mattr_accessor :format_false
+
mattr_accessor :debug
mattr_accessor :download
diff --git a/lib/effective_datatables/version.rb b/lib/effective_datatables/version.rb
index dc7e702..c6139a7 100644
--- a/lib/effective_datatables/version.rb
+++ b/lib/effective_datatables/version.rb
@@ -1,3 +1,3 @@
module EffectiveDatatables
- VERSION = '4.32.0'.freeze
+ VERSION = '4.35.3'.freeze
end