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) -> + "
#{title || ''}
#{payload}
" + + "" + + "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