diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b235eb6d..09132396 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,11 +8,13 @@ jobs:
fail-fast: false
matrix:
# To keep matrix size down, only test highest and lowest rubies.
- ruby: ["2.7", "3.3"]
- rails: ["6.1", "7.0", "7.1", "7.2"]
+ ruby: ["3.0", "3.3"]
+ rails: ["7.0", "7.1", "7.2", "8.0"]
exclude:
- - ruby: "2.7"
+ - ruby: "3.0"
rails: "7.2"
+ - ruby: "3.0"
+ rails: "8.0"
name: Ruby ${{ matrix.ruby }}, Rails ${{ matrix.rails }}
runs-on: ubuntu-latest
env:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b9285560..d9e0c68b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## 2.0.0
+
+Version 2 is a major update implementing a lot of major improvements
+at the cost of backward compatibility.
+
+[Changes and migration guide](./version-2)
+
## 1.8.3
* Fix rails hooking for version 7.1. [#327](https://github.com/bogdan/datagrid/issues/327)
diff --git a/Gemfile b/Gemfile
index 141cabfe..71afccb4 100644
--- a/Gemfile
+++ b/Gemfile
@@ -12,7 +12,10 @@ group :development do
gem "debug"
gem "nokogiri" # used to test html output
gem "pry-byebug"
+ gem "rails-dom-testing", "~> 2.2"
gem "rspec"
+ gem "rubocop", "~> 1.68"
+ gem "rubocop-yard", "~> 0.9.3", require: false
gem "sequel"
gem "sqlite3", "~> 1.7.0"
@@ -21,7 +24,3 @@ group :development do
gem "mongoid", "~> 9.0"
end
end
-
-gem "rubocop", "~> 1.68"
-
-gem "rubocop-yard", "~> 0.9.3", require: false
diff --git a/README.md b/README.md
index 628f698f..19d8fd28 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,13 @@
# Datagrid
-[![Build Status](https://github.com/bogdan/datagrid/workflows/CI/badge.svg?branch=master)](https://github.com/bogdan/datagrid/actions)
+Datagrid Version 2.0.0 is here.
-[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fbogdan%2Fdatagrid.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fbogdan%2Fdatagrid?ref=badge_shield)
+[Migration Guide](./version-2).
+
+[![Build Status](https://github.com/bogdan/datagrid/actions/workflows/ci.yml/badge.svg)](https://github.com/bogdan/datagrid/actions/workflows/ci.yml)
A really mighty and flexible ruby library that generates reports
-including admin panels, analytics and data representation:
+including admin panels, analytics and data browsers:
* Filtering
* Columns
@@ -21,8 +23,6 @@ including admin panels, analytics and data representation:
* Sequel
* Array (in-memory data of smaller scale)
-[Create an issue](https://github.com/bogdan/datagrid/issues/new) if you want more.
-
## Datagrid Philosophy
1. Expressive DSL complements OOD instead of replacing it.
@@ -31,9 +31,12 @@ including admin panels, analytics and data representation:
## Documentation
-* [Readme](/Readme.markdown) - this read-me for basic information.
-* [Wiki](https://github.com/bogdan/datagrid/wiki) - general reference on how to use the gem.
-* [Rdoc](https://rubydoc.info/gems/datagrid) - API reference.
+* [Rdoc](https://rubydoc.info/gems/datagrid) - full API reference
+* [Scope](https://rubydoc.info/gems/datagrid/Datagrid/Core) - working with datagrid scope
+* [Columns](https://rubydoc.info/gems/datagrid/Datagrid/Columns) - definging datagrid columns
+* [Filters](https://rubydoc.info/gems/datagrid/Datagrid/Filters) - defining datagrid filters
+* [Frontend](https://rubydoc.info/gems/datagrid/Datagrid/Helper) - building a frontend
+* [Configuration](https://rubydoc.info/gems/datagrid/Datagrid/Configuration) - configuring the gem
### Live Demo
@@ -47,9 +50,7 @@ including admin panels, analytics and data representation:
In order to create a grid:
``` ruby
-class UsersGrid
-
- include Datagrid
+class UsersGrid < Datagrid::Base
scope do
User.includes(:group)
@@ -122,7 +123,7 @@ scope do
end
```
-[More about scope](https://github.com/bogdan/datagrid/wiki/Scope)
+[More about scope](https://rubydoc.info/gems/datagrid/Datagrid/Core)
### Filters
@@ -146,7 +147,7 @@ Datagrid supports different type of filters including:
* string
* dynamic - build dynamic SQL condition
-[More about filters](https://github.com/bogdan/datagrid/wiki/Filters)
+[More about filters](https://rubydoc.info/gems/datagrid/Datagrid/Filters)
### Columns
@@ -161,7 +162,7 @@ end
Some formatting options are also available.
Each column is sortable.
-[More about columns](https://github.com/bogdan/datagrid/wiki/Columns)
+[More about columns](https://rubydoc.info/gems/datagrid/Datagrid/Columns)
### Front end
@@ -183,19 +184,19 @@ route resources :skills
insert app/assets/stylesheet/application.css
```
-#### Customize Built-in partials
+#### Customize Built-in views
-In order to get a control on datagrid built-in partials run:
+In order to get a control on datagrid built-in views run:
``` sh
-rake datagrid:copy_partials
+rails g datagrid::views
```
#### Advanced frontend
All advanced frontend things are described in:
-[Frontend section on wiki](https://github.com/bogdan/datagrid/wiki/Frontend)
+[Frontend section on wiki](https://rubydoc.info/gems/datagrid/Datagrid/Helper)
## Questions & Issues
diff --git a/app/assets/stylesheets/datagrid.css b/app/assets/stylesheets/datagrid.css
new file mode 100644
index 00000000..d06f1a41
--- /dev/null
+++ b/app/assets/stylesheets/datagrid.css
@@ -0,0 +1,145 @@
+/* Table */
+
+table.datagrid-table {
+ background-color: transparent;
+ border-collapse: collapse;
+ max-width: 100%;
+}
+
+table.datagrid-table th {
+ background-color: #eee;
+ text-align: left;
+ vertical-align: top;
+}
+
+table.datagrid-table td,
+table.datagrid-table th {
+ border: 1px solid #d6d6d6;
+ padding: 5px 10px;
+}
+
+.datagrid-order-control-asc,
+.datagrid-order-control-desc {
+ text-decoration: none;
+ font-weight: normal;
+}
+
+.datagrid-order-active-asc,
+.datagrid-order-active-desc {
+ background-color: #fff7d5;
+}
+
+.datagrid-order-active-asc a.datagrid-order-control-asc,
+.datagrid-order-active-desc a.datagrid-order-control-desc {
+ font-weight: bold;
+ color: #d00;
+}
+
+.datagrid-no-results {
+ text-align: center;
+}
+
+/* Form */
+
+.datagrid-form {
+ background-color: #f0f0f0;
+ border-radius: 5px;
+ padding: 20px;
+}
+
+.datagrid-filter {
+ margin: 10px;
+}
+
+.datagrid-filter label {
+ width: 150px;
+ display: inline-block;
+}
+
+.datagrid-filter input, .datagrid-filter select {
+ border: 2px solid #ccc;
+ border-radius: 4px;
+ display: inline-block;
+ padding: 5px 12px;
+ width: 300px;
+}
+
+input.datagrid-range-from, input.datagrid-range-to {
+ width: 138px;
+}
+
+.datagrid-filter select[multiple] {
+ border: 2px solid #ccc;
+ border-radius: 5px;
+ height: 100px;
+}
+
+select.datagrid-dynamic-field {
+ width: 228px
+}
+
+select.datagrid-dynamic-operation {
+ margin-left: 7px;
+ width: 60px;
+}
+
+.datagrid-dynamic-value {
+ margin: 10px 0 0 154px;
+}
+
+.datagrid-range-separator {
+ display: inline-block;
+ margin: 6px 4px 0;
+}
+
+.datagrid-enum-checkboxes {
+ display: inline-block;
+}
+
+.datagrid-enum-checkboxes input {
+ margin: 7px;
+ width: auto;
+}
+
+.datagrid-enum-checkboxes label {
+ display: block;
+ width: 100%;
+}
+
+.datagrid-actions {
+ padding-left: calc(150px + 10px);
+}
+
+.datagrid-submit {
+ background-color: #555;
+ border: none;
+ border-radius: 5px;
+ color: white;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: bold;
+ line-height: normal;
+ padding: 7px 15px;
+ vertical-align: middle;
+ display: inline-block;
+ zoom: 1;
+ *display: inline;
+}
+
+.datagrid-submit:hover,
+.datagrid-submit:focus {
+ background-color: #333;
+}
+
+.datagrid-submit:active {
+ background-color: #000;
+}
+
+.datagrid-reset {
+ font-size: 14px;
+ padding: 7px 15px;
+ vertical-align: middle;
+ display: inline-block;
+ zoom: 1;
+ *display: inline;
+}
diff --git a/app/assets/stylesheets/datagrid.sass b/app/assets/stylesheets/datagrid.sass
deleted file mode 100644
index 63fa4d61..00000000
--- a/app/assets/stylesheets/datagrid.sass
+++ /dev/null
@@ -1,135 +0,0 @@
-$dg-form-label: 150px
-
-= clearfix
- *zoom: 1
-
- &:before,
- &:after
- display: table
- content: ''
-
- &:after
- clear: both
-
-=inline-block
- display: inline-block
- zoom: 1
- *display: inline
-
-table.datagrid
- background-color: transparent
- border-collapse: collapse
- max-width: 100%
-
- th
- background-color: #eee
- text-align: left
-
- td,
- th
- border: 1px solid #d6d6d6
- padding: 5px 10px
-
- .order
- a.asc, a.desc
- text-decoration: none
- font-weight: normal
-
- &.ordered
- background-color: #fff7d5
-
- &.asc
- a.asc
- font-weight: bold
- color: #d00
-
- &.desc
- a.desc
- font-weight: bold
- color: #d00
- .noresults
- text-align: center
-
-.datagrid-form
- background-color: #f0f0f0
- border-radius: 5px
- padding: 20px
-
-.datagrid-filter
- margin: 10px
- +clearfix
-
- label
- width: $dg-form-label
- float: left
- a
- float: left
-
- input[class*='filter']
- border: 2px solid #ccc
- border-radius: 4px
- float: left
- padding: 5px 12px
- width: 207px
-
- &.from, &.to
- width: 83px
-
- select
- float: left
- width: 235px
-
- &[multiple]
- border: 2px solid #ccc
- border-radius: 5px
- height: 100px
- &.dynamic_filter
- &.field
- width: 178px
- &.operation
- margin-left: 7px
- width: 50px
- input.dynamic_filter.value
- margin: 10px 0 0 $dg-form-label
- clear: both
-
- .separator
- float: left
- margin: 6px 4px 0
- .enum_filter.checkboxes
- float: none
- display: block
- width: 100%
- input
- margin: 7px
- margin-left: 150px
-
-
-.datagrid-actions
- padding-left: $dg-form-label + 10
-
- input[type='submit']
- background-color: #555
- border: none
- border-radius: 5px
- color: white
- cursor: pointer
- font-size: 14px
- font-weight: bold
- line-height: normal
- padding: 7px 15px
- vertical-align: middle
- +inline-block
-
- &:hover,
- &:focus
- background-color: #333
-
- &:active
- background-color: #000
-
- > a
- font-size: 14px
- padding: 7px 15px
- vertical-align: middle
- +inline-block
diff --git a/app/views/datagrid/_enum_checkboxes.html.erb b/app/views/datagrid/_enum_checkboxes.html.erb
index 9f483196..281bb6f6 100644
--- a/app/views/datagrid/_enum_checkboxes.html.erb
+++ b/app/views/datagrid/_enum_checkboxes.html.erb
@@ -2,10 +2,12 @@
Indent in this file may cause extra space to appear.
You can add indent if whitespace doesn't matter for you
%>
-<%- elements.each do |value, text, checked| -%>
+
+<%- choices.each do |value, text| -%>
<%- id = [form.object_name, filter.name, value].join('_').underscore -%>
-<%= form.label filter.name, options.merge(for: id) do -%>
-<%= form.check_box(filter.name, {multiple: true, id: id, checked: checked, include_hidden: false}, value.to_s, nil) -%>
+<%= form.datagrid_label(filter.name, for: id, **options) do -%>
+<%= form.datagrid_filter_input(filter.name, id: id, value: value) -%>
<%= text -%>
<%- end -%>
<%- end -%>
+
diff --git a/app/views/datagrid/_form.html.erb b/app/views/datagrid/_form.html.erb
index 7e175c17..fc4f4aed 100644
--- a/app/views/datagrid/_form.html.erb
+++ b/app/views/datagrid/_form.html.erb
@@ -1,12 +1,12 @@
-<%= form_for grid, options do |f| -%>
+<%= form_with model: grid, html: {class: 'datagrid-form'}, scope: grid.param_name, method: :get, **options do |f| %>
<% grid.filters.each do |filter| %>
-
+
<%= f.datagrid_label filter %>
<%= f.datagrid_filter filter %>
<% end %>
- <%= f.submit I18n.t("datagrid.form.search").html_safe, class: "datagrid-submit" %>
- <%= link_to I18n.t('datagrid.form.reset').html_safe, url_for(grid.to_param => {}), class: "datagrid-reset" %>
+ <%= f.submit I18n.t("datagrid.form.search"), class: "datagrid-submit" %>
+ <%= link_to I18n.t('datagrid.form.reset'), url_for(grid.to_param => {}), class: "datagrid-reset" %>
<% end -%>
diff --git a/app/views/datagrid/_head.html.erb b/app/views/datagrid/_head.html.erb
index e9391289..2c59aa74 100644
--- a/app/views/datagrid/_head.html.erb
+++ b/app/views/datagrid/_head.html.erb
@@ -1,8 +1,31 @@
<% grid.html_columns(*options[:columns]).each do |column| %>
-
+ <%= tag.th(
+ # Consider maintaining consistency with datagrid/rows partial
+ "data-column": column.name,
+ **column.tag_options,
+ class: [
+ column.tag_options[:class],
+ # Adding HTML classes based on condition
+ "datagrid-order-active-asc": grid.ordered_by?(column, false),
+ "datagrid-order-active-desc": grid.ordered_by?(column, true),
+ ]
+ ) do %>
<%= column.header %>
- <%= datagrid_order_for(grid, column, options) if column.supports_order? && options[:order]%>
-
+ <% if column.supports_order? && options[:order] -%>
+
+ <%= link_to(
+ I18n.t("datagrid.table.order.asc"),
+ datagrid_order_path(grid, column, false),
+ class: "datagrid-order-control-asc"
+ ) %>
+ <%= link_to(
+ I18n.t("datagrid.table.order.desc"),
+ datagrid_order_path(grid, column, true),
+ class: "datagrid-order-control-desc"
+ ) %>
+
+ <% end -%>
+ <% end -%>
<% end %>
diff --git a/app/views/datagrid/_range_filter.html.erb b/app/views/datagrid/_range_filter.html.erb
index 7a8a1237..faa2575c 100644
--- a/app/views/datagrid/_range_filter.html.erb
+++ b/app/views/datagrid/_range_filter.html.erb
@@ -1,3 +1,5 @@
-<%= form.datagrid_filter_input(filter, **from_options) %>
-
<%= I18n.t('datagrid.filters.range.separator') %>
-<%= form.datagrid_filter_input(filter, **to_options) %>
+<%= form.datagrid_filter_input(filter, class: 'datagrid-range-from', **from_options) %>
+
<%= I18n.t('datagrid.filters.range.separator') %>
+<%# Generating id only for "from" input to make sure -%>
+<%# there is no duplicate id in DOM and click on label focuses the first input -%>
+<%= form.datagrid_filter_input(filter, class: 'datagrid-range-to', **to_options, id: nil) %>
diff --git a/app/views/datagrid/_row.html.erb b/app/views/datagrid/_row.html.erb
index f54d21cb..a2254b00 100644
--- a/app/views/datagrid/_row.html.erb
+++ b/app/views/datagrid/_row.html.erb
@@ -1,5 +1,16 @@
<% grid.html_columns(*options[:columns]).each do |column| %>
- <%= datagrid_value(grid, column, asset) %>
+ <%= tag.td(
+ datagrid_value(grid, column, asset),
+ # Consider maintaining consistency with datagrid/rows partial
+ "data-column": column.name,
+ **column.tag_options,
+ class: [
+ column.tag_options[:class],
+ # Adding HTML classes based on condition
+ "datagrid-order-active-asc": grid.ordered_by?(column, false),
+ "datagrid-order-active-desc": grid.ordered_by?(column, true),
+ ]
+ ) %>
<% end %>
diff --git a/app/views/datagrid/_table.html.erb b/app/views/datagrid/_table.html.erb
index 8708c05a..88eeafbe 100644
--- a/app/views/datagrid/_table.html.erb
+++ b/app/views/datagrid/_table.html.erb
@@ -5,18 +5,18 @@ Local variables:
* options - passed options Hash
%>
<% if grid.html_columns(*options[:columns]).any? %>
- <%= content_tag :table, options[:html] do %>
+ <%= tag.table class: 'datagrid-table', **options.fetch(:html, {}) do %>
- <%= datagrid_header(grid, options) %>
+ <%= datagrid_header(grid, **options) %>
<% if assets.any? %>
<%= datagrid_rows(grid, assets, **options) %>
<% else %>
- <%= I18n.t('datagrid.no_results').html_safe %>
+ <%= I18n.t('datagrid.no_results') %>
<% end %>
<% end %>
<% else -%>
- <%= I18n.t("datagrid.table.no_columns").html_safe %>
+ <%= I18n.t("datagrid.table.no_columns") %>
<% end %>
diff --git a/datagrid.gemspec b/datagrid.gemspec
index 737205f7..419c4d11 100644
--- a/datagrid.gemspec
+++ b/datagrid.gemspec
@@ -19,19 +19,20 @@ Gem::Specification.new do |s|
"CHANGELOG.md",
"README.md",
"datagrid.gemspec",
+ ".yardopts",
]
s.files += `git ls-files | grep -E '^(app|lib|templates)'`.split("\n")
s.homepage = "https://github.com/bogdan/datagrid"
s.licenses = ["MIT"]
- s.required_ruby_version = Gem::Requirement.new(">= 2.7")
+ s.required_ruby_version = Gem::Requirement.new(">= 3.0")
s.metadata = {
"homepage_uri" => s.homepage,
"bug_tracker_uri" => "#{s.homepage}/issues",
- "documentation_uri" => "#{s.homepage}/wiki",
- "changelog_uri" => "#{s.homepage}/blob/master/CHANGELOG.md",
+ "documentation_uri" => "https://rubydoc.info/gems/datagrid",
+ "changelog_uri" => "#{s.homepage}/blob/main/CHANGELOG.md",
"source_code_uri" => s.homepage,
"rubygems_mfa_required" => "true",
}
- s.add_dependency "railties", ">= 6.1"
+ s.add_dependency "railties", ">= 7.0"
end
diff --git a/gemfiles/rails_8.0.gemfile b/gemfiles/rails_8.0.gemfile
new file mode 100644
index 00000000..5185cbad
--- /dev/null
+++ b/gemfiles/rails_8.0.gemfile
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+# This file was generated by Appraisal
+
+source "https://rubygems.org"
+
+group :development do
+ gem "appraisal"
+ gem "bump"
+ gem "csv"
+ gem "nokogiri"
+ gem "pry-byebug"
+ gem "rails", "~> 8.0.0"
+ gem "rspec"
+ gem "sequel"
+ gem "sqlite3", "~> 2.1.0"
+
+ group :mongo do
+ gem "bson"
+ gem "mongoid", github: "mongodb/mongoid"
+ end
+end
+
+gemspec path: "../"
diff --git a/lib/datagrid.rb b/lib/datagrid.rb
index 5028e522..7a3111bb 100644
--- a/lib/datagrid.rb
+++ b/lib/datagrid.rb
@@ -4,27 +4,10 @@
require "datagrid/configuration"
require "datagrid/engine"
-# @!visibility public
module Datagrid
- extend ActiveSupport::Autoload
-
- autoload :Core
- autoload :ActiveModel
- autoload :Filters
- autoload :Columns
- autoload :ColumnNamesAttribute
- autoload :Ordering
- autoload :Configuration
-
- autoload :Helper
- autoload :FormBuilder
-
- autoload :Renderer
-
- autoload :Engine
-
# @!visibility private
def self.included(base)
+ Utils.warn_once("Including Datagrid is deprecated. Inherit Datagrid::Base instead.")
base.class_eval do
include ::Datagrid::Core
include ::Datagrid::ActiveModel
@@ -40,5 +23,7 @@ class ArgumentError < ::ArgumentError; end
class ColumnUnavailableError < StandardError; end
end
-require "datagrid/scaffold"
+require "datagrid/base"
+require "datagrid/generators/scaffold"
+require "datagrid/generators/views"
I18n.load_path << File.expand_path("datagrid/locale/en.yml", __dir__)
diff --git a/lib/datagrid/active_model.rb b/lib/datagrid/active_model.rb
index 22860e0a..bc77426b 100644
--- a/lib/datagrid/active_model.rb
+++ b/lib/datagrid/active_model.rb
@@ -3,21 +3,11 @@
module Datagrid
# Required to be ActiveModel compatible
module ActiveModel
- # @!visibility private
- def self.included(base)
- base.extend ClassMethods
- base.class_eval do
- begin
- require "active_model/naming"
- extend ::ActiveModel::Naming
- rescue LoadError
- end
- begin
- require "active_model/attributes_assignment"
- extend ::ActiveModel::AttributesAssignment
- rescue LoadError
- end
- end
+ extend ActiveSupport::Concern
+
+ included do
+ require "active_model/naming"
+ extend ::ActiveModel::Naming
end
module ClassMethods
diff --git a/lib/datagrid/base.rb b/lib/datagrid/base.rb
new file mode 100644
index 00000000..f9412418
--- /dev/null
+++ b/lib/datagrid/base.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Datagrid
+ extend ActiveSupport::Autoload
+
+ autoload :Core
+ autoload :ActiveModel
+ autoload :Filters
+ autoload :Columns
+ autoload :ColumnNamesAttribute
+ autoload :Ordering
+ autoload :Configuration
+
+ autoload :Helper
+ autoload :FormBuilder
+
+ autoload :Engine
+
+ # Main datagrid class allowing to define columns and filters on your objects
+ #
+ # @example
+ # class UsersGrid < Datagrid::Base
+ # scope { User }
+ #
+ # filter(:id, :integer)
+ # filter(:name, :string)
+ #
+ # column(:id)
+ # column(:name)
+ # end
+ class Base
+ include ::Datagrid::Core
+ include ::Datagrid::ActiveModel
+ include ::Datagrid::Filters
+ include ::Datagrid::Columns
+ include ::Datagrid::ColumnNamesAttribute
+ include ::Datagrid::Ordering
+ end
+end
diff --git a/lib/datagrid/columns.rb b/lib/datagrid/columns.rb
index c45ab741..fdd23e98 100644
--- a/lib/datagrid/columns.rb
+++ b/lib/datagrid/columns.rb
@@ -2,13 +2,12 @@
require "datagrid/utils"
require "active_support/core_ext/class/attribute"
+require "datagrid/columns/column"
module Datagrid
# Defines a column to be used for displaying data in a Datagrid.
#
- # class UserGrid
- # include Datagrid
- #
+ # class UserGrid < ApplicationGrid
# scope do
# User.order("users.created_at desc").joins(:group)
# end
@@ -201,33 +200,30 @@ module Datagrid
# presenter.user.created_at
# end
module Columns
- require "datagrid/columns/column"
-
# @!method default_column_options=(value)
- # @param [Hash] value default options passed to #column method call
- # @return [Hash] default options passed to #column method call
- # @example
- # # Disable default order
- # self.default_column_options = { order: false }
- # # Makes entire report HTML
- # self.default_column_options = { html: true }
+ # @param [Hash] value default options passed to {#column} method call
+ # @return [Hash] default options passed to {#column} method call
+ # @example Disable default order
+ # self.default_column_options = { order: false }
+ # @example Makes entire report HTML
+ # self.default_column_options = { html: true }
# @!method default_column_options
- # @return [Hash]
- # @see #default_column_options=
+ # @return [Hash] default options passed to {#column} method call
+ # @see #default_column_options=
# @!method batch_size=(value)
- # @param [Integer] value Specify a default batch size when generating CSV or just data. Default: 1000
- # @return [Integer] Specify a default batch size when generating CSV or just data.
- # @example
- # self.batch_size = 500
- # # Disable batches
- # self.batch_size = nil
- #
+ # Specify a default batch size when generating CSV or just data.
+ # @param [Integer] value a batch size when generating CSV or just data. Default: 1000
+ # @return [void]
+ # @example Set batch size to 500
+ # self.batch_size = 500
+ # @example Disable batches
+ # self.batch_size = nil
# @!method batch_size
- # @return [Integer]
- # @see #batch_size=
+ # @return [Integer]
+ # @see #batch_size=
# @visibility private
# @param [Object] base
@@ -255,35 +251,31 @@ def columns(*column_names, data: false, html: false)
filter_columns(columns_array, *column_names, data: data, html: html)
end
- # Defines new datagrid column
- #
+ # Defines a new datagrid column
# @param name [Symbol] column name
# @param query [String, nil] a string representing the query to select this column (supports only ActiveRecord)
- # @param options [Hash{Symbol => Object}] hash of options
# @param block [Block] proc to calculate a column value
+ # @option options [Boolean, String] html Determines if the column should be present
+ # in the HTML table and how it is formatted.
+ # @option options [String, Array
] order Determines if the column can be sortable and
+ # specifies the ORM ordering method.
+ # Example: `"created_at, id"` for ActiveRecord, `[:created_at, :id]` for Mongoid.
+ # @option options [String] order_desc Specifies a descending order for the column
+ # (used when `:order` cannot be easily reversed by the ORM).
+ # @option options [Boolean, Proc] order_by_value Enables Ruby-level ordering for the column.
+ # Warning: Sorting large datasets in Ruby is not recommended.
+ # If `true`, Datagrid orders by the column value.
+ # If a block is provided, Datagrid orders by the block's return value.
+ # @option options [Boolean] mandatory If `true`, the column will never be hidden by the `#column_names` selection.
+ # @option options [Symbol] before Places the column before the specified column when determining order.
+ # @option options [Symbol] after Places the column after the specified column when determining order.
+ # @option options [Boolean, Proc] if conditions when a column is available.
+ # @option options [Boolean, Proc] unless conditions when a column is not available.
+ # @option options [Symbol, Array] preload Specifies associations
+ # to preload for the column within the scope.
+ # @option options [Hash] tag_options Specifies HTML attributes for the `` or ` ` of the column.
+ # Example: `{ class: "content-align-right", "data-group": "statistics" }`.
# @return [Datagrid::Columns::Column]
- #
- # Available options:
- #
- # * html - determines if current column should be present in html table and how is it formatted
- # * order - determines if this column could be sortable and how.
- # The value of order is explicitly passed to ORM ordering method.
- # Ex: "created_at, id" for ActiveRecord, [:created_at, :id] for Mongoid
- # * order_desc - determines a descending order for given column
- # (only in case when :order can not be easily reversed by ORM)
- # * order_by_value - used in case it is easier to perform ordering at ruby level not on database level.
- # Warning: using ruby to order large datasets is very unrecommended.
- # If set to true - datagrid will use column value to order by this column
- # If block is given - datagrid will use value returned from block
- # * mandatory - if true, column will never be hidden with #column_names selection
- # * url - a proc with one argument, pass this option to easily convert the value into an URL
- # * before - determines the position of this column, by adding it before the column passed here
- # * after - determines the position of this column, by adding it after the column passed here
- # * if - the column is shown if the reult of calling this argument is true
- # * unless - the column is shown unless the reult of calling this argument is true
- # * preload - spefies which associations of the scope should be preloaded for this column
- #
- # @see https://github.com/bogdan/datagrid/wiki/Columns
def column(name, query = nil, **options, &block)
define_column(columns_array, name, query, **options, &block)
end
@@ -312,7 +304,7 @@ def respond_to(&block)
# @example
# column(:name) do |model|
# format(model.name) do |value|
- # content_tag(:strong, value)
+ # tag.strong(value)
# end
# end
def format(value, &block)
@@ -333,10 +325,10 @@ def format(value, &block)
# Defines a model decorator that will be used to define a column value.
# All column blocks will be given a decorated version of the model.
# @return [void]
- # @example
+ # @example Wrapping a model with presenter
# decorate { |user| UserPresenter.new(user) }
- #
- # decorate { UserPresenter } # a shortcut
+ # @example A shortcut for doing the same
+ # decorate { UserPresenter }
def decorate(model = nil, &block)
if !model && !block
raise ArgumentError, "decorate needs either a block to define decoration or a model to decorate"
@@ -403,14 +395,16 @@ def assets
)
end
- # @param column_names [Array] list of column names if you want to limit data only to specified columns
+ # @param column_names [Array] list of column names
+ # if you want to limit data only to specified columns
# @return [Array] human readable column names. See also "Localization" section
def header(*column_names)
data_columns(*column_names).map(&:header)
end
# @param asset [Object] asset from datagrid scope
- # @param column_names [Array] list of column names if you want to limit data only to specified columns
+ # @param column_names [Array] list of column names
+ # if you want to limit data only to specified columns
# @return [Array] column values for given asset
def row_for(asset, *column_names)
data_columns(*column_names).map do |column|
@@ -419,7 +413,8 @@ def row_for(asset, *column_names)
end
# @param asset [Object] asset from datagrid scope
- # @return [Hash] A mapping where keys are column names and values are column values for the given asset
+ # @return [Hash] A mapping where keys are column names and
+ # values are column values for the given asset
def hash_for(asset)
result = {}
data_columns.each do |column|
@@ -428,7 +423,8 @@ def hash_for(asset)
result
end
- # @param column_names [Array] list of column names if you want to limit data only to specified columns
+ # @param column_names [Array] list of column names
+ # if you want to limit data only to specified columns
# @return [Array>] with data for each row in datagrid assets without header
def rows(*column_names)
map_with_batches do |asset|
@@ -436,7 +432,8 @@ def rows(*column_names)
end
end
- # @param column_names [Array] list of column names if you want to limit data only to specified columns
+ # @param column_names [Array] list of column names
+ # if you want to limit data only to specified columns.
# @return [Array>] data for each row in datagrid assets with header.
def data(*column_names)
rows(*column_names).unshift(header(*column_names))
@@ -461,7 +458,7 @@ def data_hash
end
end
- # @param column_names [Array]
+ # @param column_names [Array]
# @param options [Hash] CSV generation options
# @return [String] a CSV representation of the data in the grid
#
@@ -499,7 +496,8 @@ def columns(*column_names, data: false, html: false)
end
end
- # @param column_names [Array] list of column names if you want to limit only to specified columns
+ # @param column_names [Array] list of column names
+ # if you want to limit data only to specified columns
# @param [Boolean] html return only HTML columns
# @return [Array] columns that can be represented in plain data(non-html) way
def data_columns(*column_names, html: false)
@@ -513,7 +511,7 @@ def html_columns(*column_names, data: false)
columns(*column_names, data: data, html: true)
end
- # Finds a column definition by name
+ # Finds a column by name
# @param name [String, Symbol] column name to be found
# @return [Datagrid::Columns::Column, nil]
def column_by_name(name)
@@ -521,22 +519,21 @@ def column_by_name(name)
end
# Gives ability to have a different formatting for CSV and HTML column value.
- #
- # @example
+ # @example Formating using Rails view helpers
# column(:name) do |model|
# format(model.name) do |value|
- # content_tag(:strong, value)
+ # tag.strong(value)
# end
# end
- #
+ # @example Formatting using a separated view template
# column(:company) do |model|
# format(model.company.name) do
- # render partial: "company_with_logo", locals: {company: model.company }
+ # render partial: "companies/company_with_logo", locals: {company: model.company }
# end
# end
# @return [Datagrid::Columns::Column::ResponseFormat] Format object
def format(value, &block)
- if block_given?
+ if block
self.class.format(value, &block)
else
# don't override Object#format method
@@ -544,26 +541,26 @@ def format(value, &block)
end
end
+ # @param [Object] asset one of the assets from grid scope
# @return [Datagrid::Columns::DataRow] an object representing a grid row.
# @example
- # class MyGrid
- # scope { User }
- # column(:id)
- # column(:name)
- # column(:number_of_purchases) do |user|
- # user.purchases.count
- # end
- # end
+ # class MyGrid
+ # scope { User }
+ # column(:id)
+ # column(:name)
+ # column(:number_of_purchases) do |user|
+ # user.purchases.count
+ # end
+ # end
#
- # row = MyGrid.new.data_row(User.last)
- # row.id # => user.id
- # row.number_of_purchases # => user.purchases.count
+ # row = MyGrid.new.data_row(User.last)
+ # row.id # => user.id
+ # row.number_of_purchases # => user.purchases.count
def data_row(asset)
::Datagrid::Columns::DataRow.new(self, asset)
end
# Defines a column at instance level
- #
# @see Datagrid::Columns::ClassMethods#column
def column(name, query = nil, **options, &block)
self.class.define_column(columns_array, name, query, **options, &block)
@@ -575,8 +572,8 @@ def initialize(*)
super
end
- # @return [Array] all columns that are possible to be displayed for the current grid object
- #
+ # @return [Array] all columns
+ # that are possible to be displayed for the current grid object
# @example
# class MyGrid
# filter(:search) {|scope, value| scope.full_text_search(value)}
@@ -598,6 +595,8 @@ def available_columns
end
end
+ # @param [String,Symbol] column_name column name
+ # @param [Object] asset one of the assets from grid scope
# @return [Object] a cell data value for given column name and asset
def data_value(column_name, asset)
column = column_by_name(column_name)
@@ -609,6 +608,9 @@ def data_value(column_name, asset)
end
end
+ # @param [String,Symbol] column_name column name
+ # @param [Object] asset one of the assets from grid scope
+ # @param [ActionView::Base] context view context object
# @return [Object] a cell HTML value for given column name and asset and view context
def html_value(column_name, context, asset)
column = column_by_name(column_name)
@@ -622,6 +624,7 @@ def html_value(column_name, context, asset)
end
end
+ # @param [Object] model one of the assets from grid scope
# @return [Object] a decorated version of given model if decorator is specified or the model otherwise.
def decorate(model)
self.class.decorate(model)
@@ -679,7 +682,9 @@ def cache_key(asset)
end
rescue NotImplementedError
raise Datagrid::ConfigurationError,
- "#{self} is setup to use cache. But there was appropriate cache key found for #{asset.inspect}. Please set cached option to block with asset as argument and cache key as returning value to resolve the issue."
+ <<~MSG
+ #{self} is setup to use cache. But there was appropriate cache key found for #{asset.inspect}.
+ MSG
end
def map_with_batches(&block)
@@ -727,6 +732,10 @@ def initialize(grid, model)
def method_missing(meth, *_args)
@grid.data_value(meth, @model)
end
+
+ def respond_to_missing?(meth, include_private = false)
+ !!@grid.column_by_name(meth) || super
+ end
end
end
end
diff --git a/lib/datagrid/columns/column.rb b/lib/datagrid/columns/column.rb
index 2f153592..f41d4101 100644
--- a/lib/datagrid/columns/column.rb
+++ b/lib/datagrid/columns/column.rb
@@ -39,6 +39,15 @@ def initialize(grid_class, name, query, options = {}, &block)
self.grid_class = grid_class
self.name = name.to_sym
self.options = options
+ if options[:class]
+ Datagrid::Utils.warn_once(
+ "column[class] option is deprecated. Use {tag_options: {class: ...}} instead.",
+ )
+ self.options[:tag_options] = {
+ **self.options.fetch(:tag_options, {}),
+ class: options[:class],
+ }
+ end
if options[:html] == true
self.html_block = block
else
@@ -108,6 +117,17 @@ def mandatory?
!!options[:mandatory]
end
+ def tag_options
+ options[:tag_options] || {}
+ end
+
+ def html_class
+ Datagrid::Utils.warn_once(
+ "Column#html_class is deprecated. Use Column#tag_options instead.",
+ )
+ options[:class]
+ end
+
def mandatory_explicitly_set?
options.key?(:mandatory)
end
diff --git a/lib/datagrid/configuration.rb b/lib/datagrid/configuration.rb
index 5c64e3e0..a80e8b22 100644
--- a/lib/datagrid/configuration.rb
+++ b/lib/datagrid/configuration.rb
@@ -25,12 +25,14 @@ def self.configure(&block)
# Datagrid.configure do |config|
# # Defines date formats that can be used to parse dates.
# # Note: Multiple formats can be specified. The first format is used to format dates as strings,
- # # while other formats are used only for parsing dates from strings (e.g., if your app supports multiple formats).
+ # # while other formats are used only for parsing dates
+ # # from strings (e.g., if your app supports multiple formats).
# config.date_formats = "%m/%d/%Y", "%Y-%m-%d"
#
# # Defines timestamp formats that can be used to parse timestamps.
# # Note: Multiple formats can be specified. The first format is used to format timestamps as strings,
- # # while other formats are used only for parsing timestamps from strings (e.g., if your app supports multiple formats).
+ # # while other formats are used only for parsing timestamps
+ # # from strings (e.g., if your app supports multiple formats).
# config.datetime_formats = ["%m/%d/%Y %h:%M", "%Y-%m-%d %h:%M:%s"]
# end
# ```
diff --git a/lib/datagrid/core.rb b/lib/datagrid/core.rb
index 46d58f2e..e13d6b17 100644
--- a/lib/datagrid/core.rb
+++ b/lib/datagrid/core.rb
@@ -61,20 +61,18 @@ module ClassMethods
def datagrid_attribute(name, &block)
return if datagrid_attributes.include?(name)
- block ||= lambda do |value|
- value
- end
datagrid_attributes << name
define_method name do
instance_variable_get("@#{name}")
end
define_method :"#{name}=" do |value|
- instance_variable_set("@#{name}", instance_exec(value, &block))
+ instance_variable_set("@#{name}", block ? instance_exec(value, &block) : value)
end
end
- # @return [void] Defines a scope at class level
+ # Defines a relation scope of database models to be filtered
+ # @return [void]
# @example
# scope { User }
# scope { Project.where(deleted: false) }
@@ -146,13 +144,15 @@ def dynamic(&block)
end
end
- protected
-
+ # @!visibility private
def check_scope_defined!(message = nil)
message ||= "#{self}.scope is not defined"
raise(Datagrid::ConfigurationError, message) unless scope_value
end
+ protected
+
+ # @!visibility private
def inherited(child_class)
super
child_class.datagrid_attributes = datagrid_attributes.clone
@@ -174,6 +174,15 @@ def initialize(attributes = nil, &block)
end
# @return [Hash{Symbol => Object}] grid attributes including filter values and ordering values
+ # @example
+ # class UsersGrid < ApplicationGrid
+ # scope { User }
+ # filter(:first_name, :string)
+ # filter(:last_name, :string)
+ # end
+ #
+ # grid = UsersGrid.new(first_name: 'John', last_name: 'Smith')
+ # grid.attributes # => {first_name: 'John', last_name: 'Smith', order: nil, descending: nil}
def attributes
result = {}
datagrid_attributes.each do |name|
@@ -182,14 +191,6 @@ def attributes
result
end
- # Updates datagrid attributes with a passed hash argument
- # @param attributes [Hash{Symbol => Object}]
- # @example
- # grid = MyGrid.new
- # grid.attributes = {first_name: 'John', last_name: 'Smith'}
- # grid.first_name # => 'John'
- # grid.last_name # => 'Smith'
-
# @param [String, Symbol] attribute attribute name
# @return [Object] Any datagrid attribute value
def [](attribute)
@@ -258,7 +259,7 @@ def scope(&block)
# @!visibility private
def original_scope
- check_scope_defined!
+ self.class.check_scope_defined!
scope_value.call
end
@@ -277,11 +278,6 @@ def driver
self.class.driver
end
- # @!visibility private
- def check_scope_defined!(message = nil)
- self.class.send :check_scope_defined!, message
- end
-
# @return [String] a datagrid attributes and their values in inspection form
def inspect
attrs = attributes.map do |key, value|
@@ -304,7 +300,13 @@ def reset
protected
def sanitize_for_mass_assignment(attributes)
- forbidden_attributes_protection ? super : attributes
+ if forbidden_attributes_protection
+ super
+ elsif defined?(ActionController::Parameters) && attributes.is_a?(ActionController::Parameters)
+ attributes.to_unsafe_h
+ else
+ attributes
+ end
end
end
end
diff --git a/lib/datagrid/deprecated_object.rb b/lib/datagrid/deprecated_object.rb
new file mode 100644
index 00000000..aa316460
--- /dev/null
+++ b/lib/datagrid/deprecated_object.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Datagrid
+ # @!visibility private
+ class DeprecatedObject < BasicObject
+ def initialize(real_object, &block)
+ @real_object = real_object
+ @block = block
+ end
+
+ def method_missing(method_name, ...)
+ @block.call
+ @real_object.public_send(method_name, ...)
+ end
+
+ def respond_to_missing?(method_name, include_private = false)
+ @real_object.respond_to?(method_name, include_private)
+ end
+ end
+end
diff --git a/lib/datagrid/drivers/abstract_driver.rb b/lib/datagrid/drivers/abstract_driver.rb
index 8cf5e6bc..5b92409b 100644
--- a/lib/datagrid/drivers/abstract_driver.rb
+++ b/lib/datagrid/drivers/abstract_driver.rb
@@ -93,13 +93,6 @@ def default_cache_key(asset)
raise NotImplementedError
end
- def where_by_timestamp_gotcha(scope, name, value)
- value = Datagrid::Utils.format_date_as_timestamp(value)
- scope = greater_equal(scope, name, value.first) if value.first
- scope = less_equal(scope, name, value.last) if value.last
- scope
- end
-
def default_preload(scope, value)
raise NotImplementedError
end
diff --git a/lib/datagrid/drivers/active_record.rb b/lib/datagrid/drivers/active_record.rb
index fb524bf5..bf10cac5 100644
--- a/lib/datagrid/drivers/active_record.rb
+++ b/lib/datagrid/drivers/active_record.rb
@@ -32,9 +32,7 @@ def to_scope(scope)
def append_column_queries(assets, columns)
if columns.present?
- if assets.select_values.empty?
- assets = assets.select(Arel.respond_to?(:star) ? assets.klass.arel_table[Arel.star] : "#{assets.quoted_table_name}.*")
- end
+ assets = assets.select(assets.klass.arel_table[Arel.star]) if assets.select_values.empty?
columns = columns.map { |c| "#{c.query} AS #{c.name}" }
assets = assets.select(*columns)
end
@@ -144,11 +142,3 @@ def contains_predicate
end
end
end
-
-if defined?(ActiveRecord::Base)
- ActiveRecord::Base.class_eval do
- def self.datagrid_where_by_timestamp(column, value)
- Datagrid::Drivers::ActiveRecord.new.where_by_timestamp_gotcha(self, column, value)
- end
- end
-end
diff --git a/lib/datagrid/filters.rb b/lib/datagrid/filters.rb
index c7fe692f..19637c8e 100644
--- a/lib/datagrid/filters.rb
+++ b/lib/datagrid/filters.rb
@@ -6,9 +6,7 @@ module Datagrid
# Defines the accessible attribute that is used to filter
# the scope by the specified value with specified code.
#
- # class UserGrid
- # include Datagrid
- #
+ # class UserGrid < ApplicationGrid
# scope do
# User
# end
@@ -64,13 +62,13 @@ module Datagrid
#
# `:date` - Converts value to a date. Supports the `:range` option to accept date ranges.
#
- # filter(:created_at, :date, range: true, default: proc { [1.month.ago.to_date, Date.today] })
+ # filter(:created_at, :date, range: true, default: proc { 1.month.ago.to_date..Date.today })
#
# ## Datetime
#
# `:datetime` - Converts value to a timestamp. Supports the `:range` option to accept time ranges.
#
- # filter(:created_at, :datetime, range: true, default: proc { [1.hour.ago, Time.now] })
+ # filter(:created_at, :datetime, range: true, default: proc { 1.hour.ago..Time.now })
#
# ## Enum
#
@@ -93,7 +91,7 @@ module Datagrid
#
# `:integer` - Converts value to an integer. Supports the `:range` option.
#
- # filter(:posts_count, :integer, range: true, default: [1, nil])
+ # filter(:posts_count, :integer, range: true, default: (1..nil))
#
# ## String
#
@@ -123,7 +121,7 @@ module Datagrid
# Example:
#
# filter(:id, :integer, header: "Identifier")
- # filter(:created_at, :date, range: true, default: proc { [1.month.ago.to_date, Date.today] })
+ # filter(:created_at, :date, range: true, default: proc { 1.month.ago.to_date..Date.today })
#
# # Localization
#
@@ -140,7 +138,6 @@ module Filters
require "datagrid/filters/date_time_filter"
require "datagrid/filters/default_filter"
require "datagrid/filters/integer_filter"
- require "datagrid/filters/composite_filters"
require "datagrid/filters/string_filter"
require "datagrid/filters/float_filter"
require "datagrid/filters/dynamic_filter"
@@ -162,16 +159,14 @@ module Filters
# @!visibility private
DEFAULT_FILTER_BLOCK = Object.new
- # @!visibility private
- def self.included(base)
- base.extend ClassMethods
- base.class_eval do
- include Datagrid::Core
- include Datagrid::Filters::CompositeFilters
- class_attribute :filters_array, default: []
- end
+ extend ActiveSupport::Concern
+
+ included do
+ include Datagrid::Core
+ class_attribute :filters_array, default: []
end
+ # Grid class methods related to filters
module ClassMethods
# @return [Datagrid::Filters::BaseFilter, nil] filter definition object by name
def filter_by_name(attribute)
@@ -209,8 +204,8 @@ def filter_by_name(attribute)
# Used with the `datagrid_form_for` helper.
# @option options [Symbol] after Specifies the position of this filter by placing it after another filter.
# Used with the `datagrid_form_for` helper.
- # @option options [Boolean] dummy If true, the filter is not applied automatically and is only displayed in the form.
- # Useful for manual application.
+ # @option options [Boolean] dummy If true, the filter is not applied automatically and
+ # is only displayed in the form. Useful for manual application.
# @option options [Proc, Symbol] if Specifies a condition under which the filter is displayed and used.
# Accepts a block or the name of an instance method.
# @option options [Proc, Symbol] unless Specifies a condition under which the filter is NOT displayed or used.
@@ -223,7 +218,7 @@ def filter(name, type = :default, **options, &block)
raise ConfigurationError, "filter class #{type.inspect} not found" unless klass
position = Datagrid::Utils.extract_position_from_options(filters_array, options)
- filter = klass.new(self, name, options, &block)
+ filter = klass.new(self, name, **options, &block)
filters_array.insert(position, filter)
datagrid_attribute(name) do |value|
@@ -267,7 +262,8 @@ def filters_inspection
def initialize(...)
self.filters_array = self.class.filters_array.clone
filters_array.each do |filter|
- self[filter.name] = filter.default(self)
+ value = filter.default(self)
+ self[filter.name] = value unless value.nil?
end
super
end
@@ -337,8 +333,11 @@ def default_filter
def find_select_filter(filter)
filter = filter_by_name(filter)
unless filter.class.included_modules.include?(::Datagrid::Filters::SelectOptions)
- raise ::Datagrid::ArgumentError,
- "#{self.class.name}##{filter.name} with type #{FILTER_TYPES.invert[filter.class].inspect} can not have select options"
+ type = FILTER_TYPES.invert[filter.class].inspect
+ raise(
+ ::Datagrid::ArgumentError,
+ "#{self.class.name}##{filter.name} with type #{type} can not have select options",
+ )
end
filter
end
diff --git a/lib/datagrid/filters/base_filter.rb b/lib/datagrid/filters/base_filter.rb
index eb80c39f..56303f11 100644
--- a/lib/datagrid/filters/base_filter.rb
+++ b/lib/datagrid/filters/base_filter.rb
@@ -10,20 +10,23 @@ class FilteringError < StandardError
module Datagrid
module Filters
class BaseFilter
- class_attribute :input_helper_name, instance_writer: false
attr_accessor :grid_class, :options, :block, :name
- def initialize(grid_class, name, options = {}, &block)
+ def initialize(grid_class, name, **options, &block)
self.grid_class = grid_class
self.name = name.to_sym
self.options = options
- self.block = block || default_filter_block
+ self.block = block
end
def parse(value)
raise NotImplementedError, "#parse(value) suppose to be overwritten"
end
+ def default_input_options
+ { type: "text" }
+ end
+
def unapplicable_value?(value)
value.nil? ? !allow_nil? : value.blank? && !allow_blank?
end
@@ -35,10 +38,12 @@ def apply(grid_object, scope, value)
return scope unless result
- result = default_filter(value, scope, grid_object) if result == Datagrid::Filters::DEFAULT_FILTER_BLOCK
+ result = default_filter(value, scope) if result == Datagrid::Filters::DEFAULT_FILTER_BLOCK
unless grid_object.driver.match?(result)
- raise Datagrid::FilteringError,
- "Can not apply #{name.inspect} filter: result #{result.inspect} no longer match #{grid_object.driver.class}."
+ raise(
+ Datagrid::FilteringError,
+ "Filter #{name.inspect} unapplicable: result no longer match #{grid_object.driver.class}.",
+ )
end
result
@@ -48,12 +53,18 @@ def parse_values(value)
if multiple?
return nil if value.nil?
- normalize_multiple_value(value).map do |v|
+ return normalize_multiple_value(value).map do |v|
parse(v)
end
- elsif value.is_a?(Array)
+ end
+
+ case value
+ when Array
raise Datagrid::ArgumentError,
"#{grid_class}##{name} filter can not accept Array argument. Use :multiple option."
+ when Range
+ raise Datagrid::ArgumentError,
+ "#{grid_class}##{name} filter can not accept Range argument. Use :range option."
else
parse(value)
end
@@ -86,6 +97,10 @@ def multiple?
options[:multiple]
end
+ def range?
+ false
+ end
+
def allow_nil?
options.key?(:allow_nil) ? options[:allow_nil] : options[:allow_blank]
end
@@ -110,13 +125,6 @@ def self.form_builder_helper_name
:"datagrid_#{to_s.demodulize.underscore}"
end
- def default_filter_block
- filter = self
- lambda do |value, scope, grid|
- filter.default_filter(value, scope, grid)
- end
- end
-
def supports_range?
self.class.ancestors.include?(::Datagrid::Filters::RangedFilter)
end
@@ -140,6 +148,14 @@ def enabled?(grid)
::Datagrid::Utils.process_availability(grid, options[:if], options[:unless])
end
+ def enum_checkboxes?
+ false
+ end
+
+ def default_scope?
+ !block
+ end
+
protected
def default_filter_where(scope, value)
@@ -147,10 +163,12 @@ def default_filter_where(scope, value)
end
def execute(value, scope, grid_object)
- if block.arity == 1
+ if block&.arity == 1
scope.instance_exec(value, &block)
- else
+ elsif block
Datagrid::Utils.apply_args(value, scope, grid_object, &block)
+ else
+ default_filter(value, scope)
end
end
@@ -175,7 +193,7 @@ def driver
grid_class.driver
end
- def default_filter(value, scope, _grid)
+ def default_filter(value, scope)
return nil if dummy?
if !driver.scope_has_column?(scope, name) && scope.respond_to?(name, true)
diff --git a/lib/datagrid/filters/boolean_filter.rb b/lib/datagrid/filters/boolean_filter.rb
index d7e40b44..753f3da9 100644
--- a/lib/datagrid/filters/boolean_filter.rb
+++ b/lib/datagrid/filters/boolean_filter.rb
@@ -5,6 +5,16 @@
module Datagrid
module Filters
class BooleanFilter < Datagrid::Filters::BaseFilter
+ # @!visibility private
+ def initialize(grid, name, **opts)
+ super
+ options[:default] ||= false
+ end
+
+ def default_input_options
+ { **super, type: "checkbox" }
+ end
+
def parse(value)
Datagrid::Utils.booleanize(value)
end
diff --git a/lib/datagrid/filters/composite_filters.rb b/lib/datagrid/filters/composite_filters.rb
deleted file mode 100644
index afa48d66..00000000
--- a/lib/datagrid/filters/composite_filters.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# frozen_string_literal: true
-
-module Datagrid
- module Filters
- # @!visibility private
- module CompositeFilters
- def self.included(base)
- base.extend ClassMethods
- base.class_eval do
- end
- end
-
- # @!visibility private
- module ClassMethods
- def date_range_filters(field, from_options = {}, to_options = {})
- Utils.warn_once("date_range_filters is deprecated in favor of range option for date filter")
- from_options = normalize_composite_filter_options(from_options, field)
- to_options = normalize_composite_filter_options(to_options, field)
-
- filter(from_options[:name] || :"from_#{field.to_s.tr('.', '_')}", :date,
-**from_options,) do |date, scope, grid|
- grid.driver.greater_equal(scope, field, date)
- end
- filter(to_options[:name] || :"to_#{field.to_s.tr('.', '_')}", :date, **to_options) do |date, scope, grid|
- grid.driver.less_equal(scope, field, date)
- end
- end
-
- def integer_range_filters(field, from_options = {}, to_options = {})
- Utils.warn_once("integer_range_filters is deprecated in favor of range option for integer filter")
- from_options = normalize_composite_filter_options(from_options, field)
- to_options = normalize_composite_filter_options(to_options, field)
- filter(from_options[:name] || :"from_#{field.to_s.tr('.', '_')}", :integer,
-**from_options,) do |value, scope, grid|
- grid.driver.greater_equal(scope, field, value)
- end
- filter(to_options[:name] || :"to_#{field.to_s.tr('.', '_')}", :integer, **to_options) do |value, scope, grid|
- grid.driver.less_equal(scope, field, value)
- end
- end
-
- def normalize_composite_filter_options(options, _field)
- options = { name: options } if options.is_a?(String) || options.is_a?(Symbol)
- options
- end
- end
- end
- end
-end
diff --git a/lib/datagrid/filters/date_filter.rb b/lib/datagrid/filters/date_filter.rb
index 12057178..e8462268 100644
--- a/lib/datagrid/filters/date_filter.rb
+++ b/lib/datagrid/filters/date_filter.rb
@@ -7,8 +7,12 @@ module Filters
class DateFilter < Datagrid::Filters::BaseFilter
include Datagrid::Filters::RangedFilter
+ def default_input_options
+ { **super, type: "date" }
+ end
+
def apply(grid_object, scope, value)
- value = value.begin&.beginning_of_day..value.end&.end_of_day if value.is_a?(Range)
+ value = Datagrid::Utils.format_date_as_timestamp(value) if grid_object.driver.timestamp_column?(scope, name)
super
end
@@ -24,11 +28,6 @@ def format(value)
end
end
- def default_filter_where(scope, value)
- value = Datagrid::Utils.format_date_as_timestamp(value) if driver.timestamp_column?(scope, name)
- super
- end
-
protected
def formats
diff --git a/lib/datagrid/filters/date_time_filter.rb b/lib/datagrid/filters/date_time_filter.rb
index 0014eb1d..55e4e44d 100644
--- a/lib/datagrid/filters/date_time_filter.rb
+++ b/lib/datagrid/filters/date_time_filter.rb
@@ -7,6 +7,10 @@ module Filters
class DateTimeFilter < Datagrid::Filters::BaseFilter
include Datagrid::Filters::RangedFilter
+ def default_input_options
+ { **super, type: "datetime-local" }
+ end
+
def parse(value)
Datagrid::Utils.parse_datetime(value)
end
diff --git a/lib/datagrid/filters/dynamic_filter.rb b/lib/datagrid/filters/dynamic_filter.rb
index 7487ab8a..967a37d4 100644
--- a/lib/datagrid/filters/dynamic_filter.rb
+++ b/lib/datagrid/filters/dynamic_filter.rb
@@ -19,28 +19,29 @@ class DynamicFilter < Datagrid::Filters::BaseFilter
].freeze
AVAILABLE_OPERATIONS = %w[= =~ >= <=].freeze
- def initialize(*)
- super
+ def initialize(grid, name, **options, &block)
options[:select] ||= default_select
options[:operations] ||= DEFAULT_OPERATIONS
- return if options.key?(:include_blank)
+ options[:include_blank] = false unless options.key?(:include_blank)
+ super
+ end
- options[:include_blank] = false
+ def default_input_options
+ { **super, type: nil }
end
def parse_values(filter)
- field, operation, value = filter
-
- [field, operation, type_cast(field, value)]
+ filter ? FilterValue.new(grid_class, filter) : nil
end
def unapplicable_value?(filter)
- _, _, value = filter
- super(value)
+ super(filter&.value)
end
def default_filter_where(scope, filter)
- field, operation, value = filter
+ field = filter.field
+ operation = filter.operation
+ value = filter.value
date_conversion = value.is_a?(Date) && driver.timestamp_column?(scope, field)
return scope if field.blank? || operation.blank?
@@ -79,7 +80,7 @@ def operations
def operations_select
operations.map do |operation|
- [I18n.t(operation, scope: "datagrid.filters.dynamic.operations").html_safe, operation]
+ [I18n.t(operation, scope: "datagrid.filters.dynamic.operations"), operation]
end
end
@@ -95,33 +96,76 @@ def default_select
}
end
- def type_cast(field, value)
- type = column_type(field)
- return nil if value.blank?
-
- case type
- when :string
- value.to_s
- when :integer
- value.is_a?(Numeric) || value =~ %r{^\d} ? value.to_i : nil
- when :float
- value.is_a?(Numeric) || value =~ %r{^\d} ? value.to_f : nil
- when :date
- Datagrid::Utils.parse_date(value)
- when :timestamp
- Datagrid::Utils.parse_date(value)
- when :boolean
- Datagrid::Utils.booleanize(value)
- when nil
- value
- else
- raise NotImplementedError, "unknown column type: #{type.inspect}"
- end
- end
-
def column_type(field)
grid_class.driver.normalized_column_type(grid_class.scope, field)
end
+
+ class FilterValue
+ attr_accessor :field, :operation, :value
+
+ def initialize(grid_class, object = nil)
+ super()
+
+ case object
+ when Hash
+ object = object.symbolize_keys
+ self.field = object[:field]
+ self.operation = object[:operation]
+ self.value = object[:value]
+ when Array
+ self.field = object[0]
+ self.operation = object[1]
+ self.value = object[2]
+ else
+ raise ArgumentError, object.inspect
+ end
+ return unless grid_class
+
+ type = grid_class.driver.normalized_column_type(
+ grid_class.scope, field,
+ )
+ self.value = type_cast(type, value)
+ end
+
+ def inspect
+ { field: field, operation: operation, value: value }
+ end
+
+ def to_ary
+ to_a
+ end
+
+ def to_a
+ [field, operation, value]
+ end
+
+ def to_h
+ { field: field, operation: operation, value: value }
+ end
+
+ protected
+
+ def type_cast(type, value)
+ return nil if value.blank?
+
+ case type
+ when :string
+ value.to_s
+ when :integer
+ value.is_a?(Numeric) || value =~ %r{^\d} ? value.to_i : nil
+ when :float
+ value.is_a?(Numeric) || value =~ %r{^\d} ? value.to_f : nil
+ when :date, :timestamp
+ Datagrid::Utils.parse_date(value)
+ when :boolean
+ Datagrid::Utils.booleanize(value)
+ when nil
+ value
+ else
+ raise NotImplementedError, "unknown column type: #{type.inspect}"
+ end
+ end
+ end
end
end
end
diff --git a/lib/datagrid/filters/enum_filter.rb b/lib/datagrid/filters/enum_filter.rb
index d05b215e..d21d6f49 100644
--- a/lib/datagrid/filters/enum_filter.rb
+++ b/lib/datagrid/filters/enum_filter.rb
@@ -7,9 +7,10 @@ module Filters
class EnumFilter < Datagrid::Filters::BaseFilter
include Datagrid::Filters::SelectOptions
- def initialize(*args)
+ # @!visibility private
+ def initialize(grid, name, **options, &block)
+ options[:multiple] = true if options[:checkboxes]
super
- options[:multiple] = true if checkboxes?
raise Datagrid::ConfigurationError, ":select option not specified" unless options[:select]
end
@@ -19,11 +20,31 @@ def parse(value)
value
end
+ def default_input_options
+ {
+ **super,
+ type: enum_checkboxes? ? "checkbox" : "select",
+ multiple: multiple?,
+ include_hidden: enum_checkboxes? ? false : nil,
+ }
+ end
+
+ def label_options
+ if enum_checkboxes?
+ # Each checkbox has its own label
+ # The main label has no specific input to focus
+ # See app/views/datagrid/_enum_checkboxes.html.erb
+ { for: nil, **super }
+ else
+ super
+ end
+ end
+
def strict
options[:strict]
end
- def checkboxes?
+ def enum_checkboxes?
options[:checkboxes]
end
end
diff --git a/lib/datagrid/filters/extended_boolean_filter.rb b/lib/datagrid/filters/extended_boolean_filter.rb
index e85181c1..dbd68958 100644
--- a/lib/datagrid/filters/extended_boolean_filter.rb
+++ b/lib/datagrid/filters/extended_boolean_filter.rb
@@ -8,7 +8,7 @@ class ExtendedBooleanFilter < Datagrid::Filters::EnumFilter
TRUTH_VALUES = [true, "true", "y", "yes"].freeze
FALSE_VALUES = [false, "false", "n", "no"].freeze
- def initialize(report, attribute, options = {}, &block)
+ def initialize(*args, **options)
options[:select] = -> { boolean_select }
super
end
@@ -18,6 +18,10 @@ def execute(value, scope, grid_object)
super
end
+ def default_input_options
+ { **super, type: "select" }
+ end
+
def parse(value)
value = value.downcase if value.is_a?(String)
case value
diff --git a/lib/datagrid/filters/float_filter.rb b/lib/datagrid/filters/float_filter.rb
index d84f6e12..d6b586c5 100644
--- a/lib/datagrid/filters/float_filter.rb
+++ b/lib/datagrid/filters/float_filter.rb
@@ -5,6 +5,10 @@ module Filters
class FloatFilter < Datagrid::Filters::BaseFilter
include Datagrid::Filters::RangedFilter
+ def default_input_options
+ { **super, type: "number", step: "any" }
+ end
+
def parse(value)
return nil if value.blank?
diff --git a/lib/datagrid/filters/integer_filter.rb b/lib/datagrid/filters/integer_filter.rb
index 6395b0be..26146ffc 100644
--- a/lib/datagrid/filters/integer_filter.rb
+++ b/lib/datagrid/filters/integer_filter.rb
@@ -7,6 +7,10 @@ module Filters
class IntegerFilter < Datagrid::Filters::BaseFilter
include Datagrid::Filters::RangedFilter
+ def default_input_options
+ { **super, type: "number", step: "1" }
+ end
+
def parse(value)
return nil if value.blank?
if defined?(ActiveRecord) && value.is_a?(ActiveRecord::Base) &&
@@ -15,6 +19,8 @@ def parse(value)
end
return value if value.is_a?(Range)
+ return nil if value.to_i.zero? && value.is_a?(String) && value !~ %r{\A\s*-?0}
+
value.to_i
end
end
diff --git a/lib/datagrid/filters/ranged_filter.rb b/lib/datagrid/filters/ranged_filter.rb
index 9fa95d2d..5b3fac00 100644
--- a/lib/datagrid/filters/ranged_filter.rb
+++ b/lib/datagrid/filters/ranged_filter.rb
@@ -3,35 +3,29 @@
module Datagrid
module Filters
module RangedFilter
- def initialize(grid, name, options, &block)
- super
- return unless range?
-
- options[:multiple] = true
- end
+ SERIALIZED_RANGE = %r{\A(.*)\.{2,3}(.*)\z}
def parse_values(value)
- result = super
- return result if !range? || result.nil?
- # Simulate single point range
- return [result, result] unless result.is_a?(Array)
+ return super unless range?
- case result.size
- when 0
- nil
- when 1
- result.first
- when 2
- if result.first && result.last && result.first > result.last
- # If wrong range is given - reverse it to be always valid
- result.reverse
- elsif !result.first && !result.last
+ case value
+ when String
+ if ["..", "..."].include?(value)
nil
+ elsif (match = value.match(SERIALIZED_RANGE))
+ to_range(match.captures[0], match.captures[1], value.include?("..."))
else
- result
+ super
end
+ when Hash
+ parse_hash(value)
+ when Array
+ parse_array(value)
+ when Range
+ to_range(value.begin, value.end)
else
- raise ArgumentError, "Can not create a date range from array of more than two: #{result.inspect}"
+ result = super
+ to_range(result, result)
end
end
@@ -40,15 +34,44 @@ def range?
end
def default_filter_where(scope, value)
- if range? && value.is_a?(Array)
- left, right = value
- scope = driver.greater_equal(scope, name, left) if left
- scope = driver.less_equal(scope, name, right) if right
+ if range? && value.is_a?(Range)
+ scope = driver.greater_equal(scope, name, value.begin) if value.begin
+ scope = driver.less_equal(scope, name, value.end) if value.end
scope
else
super
end
end
+
+ protected
+
+ def parse_hash(result)
+ to_range(result[:from] || result["from"], result[:to] || result["to"])
+ end
+
+ def to_range(from, to, exclusive = false)
+ from = parse(from)
+ to = parse(to)
+ return nil unless to || from
+
+ # If wrong range is given - reverse it to be always valid
+ from, to = to, from if from && to && from > to
+ exclusive ? from...to : from..to
+ end
+
+ def parse_array(result)
+ first = result.first
+ last = result.last
+
+ case result.size
+ when 0
+ nil
+ when 1, 2
+ to_range(first, last)
+ else
+ raise ArgumentError, "Can not create a range from array of more than two elements"
+ end
+ end
end
end
end
diff --git a/lib/datagrid/form_builder.rb b/lib/datagrid/form_builder.rb
index 8bfdf61d..49106214 100644
--- a/lib/datagrid/form_builder.rb
+++ b/lib/datagrid/form_builder.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "action_view"
+require "datagrid/deprecated_object"
module Datagrid
module FormBuilder
@@ -10,11 +11,17 @@ module FormBuilder
# * select for enum, xboolean filter types
# * check_box for boolean filter type
# * text_field for other filter types
- # @param [String] partials deprecated option
- def datagrid_filter(filter_or_attribute, partials: nil, **options, &block)
+ def datagrid_filter(filter_or_attribute, **options, &block)
filter = datagrid_get_filter(filter_or_attribute)
- options = add_html_classes({ **filter.input_options, **options }, filter.name, datagrid_filter_html_class(filter))
- send(filter.form_builder_helper_name, filter, **options, &block)
+ if filter.range?
+ datagrid_range_filter(filter, options, &block)
+ elsif filter.enum_checkboxes?
+ datagrid_enum_checkboxes_filter(filter, options, &block)
+ elsif filter.type == :dynamic
+ datagrid_dynamic_filter(filter, options, &block)
+ else
+ datagrid_filter_input(filter, **options, &block)
+ end
end
# @param filter_or_attribute [Datagrid::Filters::BaseFilter, String, Symbol] filter object or filter name
@@ -23,7 +30,8 @@ def datagrid_filter(filter_or_attribute, partials: nil, **options, &block)
# @return [String] a form label tag for the corresponding filter name
def datagrid_label(filter_or_attribute, text = nil, **options, &block)
filter = datagrid_get_filter(filter_or_attribute)
- label(filter.name, text || filter.header, **filter.label_options, **options, &block)
+ options = { **filter.label_options, **options }
+ label(filter.name, text || filter.header, **options, &block)
end
# @param [Datagrid::Filters::BaseFilter, String, Symbol] attribute_or_filter filter object or filter name
@@ -31,66 +39,34 @@ def datagrid_label(filter_or_attribute, text = nil, **options, &block)
# * `type` - special attribute the determines an input tag to be made.
# Examples: `text`, `select`, `textarea`, `number`, `date` etc.
# @return [String] an input tag for the corresponding filter name
- def datagrid_filter_input(attribute_or_filter, **options)
+ def datagrid_filter_input(attribute_or_filter, **options, &block)
filter = datagrid_get_filter(attribute_or_filter)
- value = object.filter_value_as_string(filter)
- type = options[:type]&.to_sym
- if options.key?(:value) && options[:value].nil? && %i[datetime-local date].include?(type)
- # https://github.com/rails/rails/pull/53387
- options[:value] = ""
+ options = add_filter_options(filter, **options)
+ type = options.delete(:type)&.to_sym
+ if %i[datetime-local date].include?(type)
+ if options.key?(:value) && options[:value].nil?
+ # https://github.com/rails/rails/pull/53387
+ options[:value] = ""
+ end
+ elsif options[:value]
+ options[:value] = filter.format(options[:value])
end
case type
when :"datetime-local"
- datetime_local_field filter.name, **options
+ datetime_local_field filter.name, **options, &block
when :date
- date_field filter.name, **options
+ date_field filter.name, **options, &block
when :textarea
- text_area filter.name, value: value, **options, type: nil
- else
- text_field filter.name, value: value, **options
- end
- end
-
- protected
-
- def datagrid_extended_boolean_filter(filter, options = {})
- datagrid_enum_filter(filter, options)
- end
-
- def datagrid_boolean_filter(filter, options = {})
- check_box(filter.name, options)
- end
-
- def datagrid_date_filter(filter, options = {})
- datagrid_range_filter(:date, filter, options)
- end
-
- def datagrid_date_time_filter(filter, options = {})
- datagrid_range_filter(:datetime, filter, options)
- end
-
- def datagrid_default_filter(filter, options = {})
- datagrid_filter_input(filter, **options)
- end
-
- def datagrid_enum_filter(filter, options = {}, &block)
- if filter.checkboxes?
- options = add_html_classes(options, "checkboxes")
- elements = object.select_options(filter).map do |element|
- text, value = @template.send(:option_text_and_value, element)
- checked = enum_checkbox_checked?(filter, value)
- [value, text, checked]
- end
- render_partial(
- "enum_checkboxes",
- {
- elements: elements,
- form: self,
- filter: filter,
- options: options,
- },
- )
- else
+ text_area filter.name, value: object.filter_value_as_string(filter), **options, &block
+ when :checkbox
+ value = options.fetch(:value, 1).to_s
+ options = { checked: true, **options } if filter.enum_checkboxes? && enum_checkbox_checked?(filter, value)
+ check_box filter.name, options, value
+ when :hidden
+ hidden_field filter.name, **options
+ when :number
+ number_field filter.name, **options
+ when :select
select(
filter.name,
object.select_options(filter) || [],
@@ -103,11 +79,44 @@ def datagrid_enum_filter(filter, options = {}, &block)
**options,
&block
)
+ else
+ text_field filter.name, value: object.filter_value_as_string(filter), **options, &block
end
end
+ protected
+
+ def datagrid_enum_checkboxes_filter(filter, options = {})
+ elements = object.select_options(filter).map do |element|
+ text, value = @template.send(:option_text_and_value, element)
+ checked = enum_checkbox_checked?(filter, value)
+ [value, text, checked]
+ end
+ choices = elements.map do |value, text, *_|
+ [value, text]
+ end
+ render_partial(
+ "enum_checkboxes",
+ {
+ form: self,
+ elements: Datagrid::DeprecatedObject.new(
+ elements,
+ ) do
+ Datagrid::Utils.warn_once(
+ <<~MSG,
+ Using `elements` variable in enum_checkboxes view is deprecated, use `choices` instead.
+ MSG
+ )
+ end,
+ choices: choices,
+ filter: filter,
+ options: options,
+ },
+ )
+ end
+
def enum_checkbox_checked?(filter, option_value)
- current_value = object.public_send(filter.name)
+ current_value = object.filter_value(filter)
if current_value.respond_to?(:include?)
# Typecast everything to string
# to remove difference between String and Symbol
@@ -117,15 +126,9 @@ def enum_checkbox_checked?(filter, option_value)
end
end
- def datagrid_integer_filter(filter, options = {})
- options[:value] = "" if filter.multiple? && object[filter.name].blank?
- datagrid_range_filter(:integer, filter, options)
- end
-
def datagrid_dynamic_filter(filter, options = {})
- input_name = "#{object_name}[#{filter.name}][]"
field, operation, value = object.filter_value(filter)
- options = options.merge(name: input_name)
+ options = add_filter_options(filter, **options)
field_input = dynamic_filter_select(
filter.name,
object.select_options(filter) || [],
@@ -135,7 +138,8 @@ def datagrid_dynamic_filter(filter, options = {})
include_hidden: false,
selected: field,
},
- add_html_classes(options, "field"),
+ **add_html_classes(options, "datagrid-dynamic-field"),
+ name: @template.field_name(object_name, filter.name, "field"),
)
operation_input = dynamic_filter_select(
filter.name, filter.operations_select,
@@ -145,9 +149,15 @@ def datagrid_dynamic_filter(filter, options = {})
prompt: false,
selected: operation,
},
- add_html_classes(options, "operation"),
+ **add_html_classes(options, "datagrid-dynamic-operation"),
+ name: @template.field_name(object_name, filter.name, "operation"),
+ )
+ value_input = datagrid_filter_input(
+ filter.name,
+ **add_html_classes(options, "datagrid-dynamic-value"),
+ value: value,
+ name: @template.field_name(object_name, filter.name, "value"),
)
- value_input = text_field(filter.name, **add_html_classes(options, "value"), value: value)
[field_input, operation_input, value_input].join("\n").html_safe
end
@@ -163,56 +173,26 @@ def dynamic_filter_select(name, variants, select_options, html_options)
end
end
- def datagrid_range_filter(_type, filter, options = {})
- if filter.range?
- options = options.merge(multiple: true)
- from_options = datagrid_range_filter_options(object, filter, :from, options)
- to_options = datagrid_range_filter_options(object, filter, :to, options)
- render_partial "range_filter", {
- from_options: from_options, to_options: to_options, filter: filter, form: self,
- }
- else
- datagrid_filter_input(filter, **options)
- end
+ def datagrid_range_filter(filter, options = {})
+ from_options = datagrid_range_filter_options(object, filter, :from, **options)
+ to_options = datagrid_range_filter_options(object, filter, :to, **options)
+ render_partial "range_filter", {
+ from_options: from_options, to_options: to_options, filter: filter, form: self,
+ }
end
- def datagrid_range_filter_options(object, filter, type, options)
- type_method_map = { from: :first, to: :last }
- options = add_html_classes(options, type)
- options[:value] = filter.format(object[filter.name]&.public_send(type_method_map[type]))
- # In case of datagrid ranged filter
- # from and to input will have same id
- if !options.key?(:id)
- # Rails provides it's own default id for all inputs
- # In order to prevent that we assign no id by default
- options[:id] = nil
- elsif options[:id].present?
- # If the id was given we prefix it
- # with from_ and to_ accordingly
- options[:id] = [type, options[:id]].join("_")
- end
+ def datagrid_range_filter_options(object, filter, section, **options)
+ type_method_map = { from: :begin, to: :end }
+ options[:value] = object[filter.name]&.public_send(type_method_map[section])
+ options[:name] = @template.field_name(object_name, filter.name, section)
options
end
- def datagrid_string_filter(filter, options = {})
- datagrid_range_filter(:string, filter, options)
- end
-
- def datagrid_float_filter(filter, options = {})
- datagrid_range_filter(:float, filter, options)
- end
-
def datagrid_get_filter(attribute_or_filter)
- if Utils.string_like?(attribute_or_filter)
- object.class.filter_by_name(attribute_or_filter) ||
- raise(Error, "Datagrid filter #{attribute_or_filter} not found")
- else
- attribute_or_filter
- end
- end
+ return attribute_or_filter unless Utils.string_like?(attribute_or_filter)
- def datagrid_filter_html_class(filter)
- filter.class.to_s.demodulize.underscore
+ object.class.filter_by_name(attribute_or_filter) ||
+ raise(ArgumentError, "Datagrid filter #{attribute_or_filter} not found")
end
def add_html_classes(options, *classes)
@@ -232,7 +212,8 @@ def render_partial(name, locals)
@template.render partial: partial_path(name), locals: locals
end
- class Error < StandardError
+ def add_filter_options(filter, **options)
+ { **filter.default_input_options, **filter.input_options, **options }
end
end
end
diff --git a/lib/datagrid/generators/scaffold.rb b/lib/datagrid/generators/scaffold.rb
new file mode 100644
index 00000000..7b387679
--- /dev/null
+++ b/lib/datagrid/generators/scaffold.rb
@@ -0,0 +1,184 @@
+# frozen_string_literal: true
+
+require "rails/generators"
+
+module Datagrid
+ # @!visibility private
+ module Generators
+ # @!visibility private
+ class Scaffold < Rails::Generators::NamedBase
+ include Rails::Generators::ResourceHelpers
+
+ check_class_collision suffix: "Grid"
+ source_root File.expand_path("#{__FILE__}/../../../templates")
+
+ def create_scaffold
+ template "base.rb.erb", base_grid_file unless file_exists?(base_grid_file)
+ template "grid.rb.erb", "app/grids/#{grid_class_name.underscore}.rb"
+ if file_exists?(grid_controller_file)
+ inject_into_file grid_controller_file, index_action, after: %r{class .*#{grid_controller_class_name}.*\n}
+ else
+ create_file grid_controller_file, controller_code
+ end
+ create_file view_file, view_code
+ route(generate_routing_namespace("resources :#{grid_controller_short_name}"))
+ gem "kaminari" unless kaminari? || will_paginate? || pagy?
+ in_root do
+ {
+ "css" => " *= require datagrid",
+ "css.sass" => " *= require datagrid",
+ "css.scss" => " *= require datagrid",
+ }.each do |extension, string|
+ file = "app/assets/stylesheets/application.#{extension}"
+ if file_exists?(file)
+ inject_into_file file, "#{string}\n", { before: %r{.*require_self} } # before all
+ end
+ end
+ end
+ end
+
+ def view_file
+ Rails.root.join("app/views").join(controller_file_path).join("index.html.erb")
+ end
+
+ def grid_class_name
+ "#{file_name.camelize.pluralize}Grid"
+ end
+
+ def grid_base_class
+ file_exists?("app/grids/base_grid.rb") ? "BaseGrid" : "ApplicationGrid"
+ end
+
+ def grid_controller_class_name
+ "#{controller_class_name.camelize}Controller"
+ end
+
+ def grid_controller_file
+ Rails.root.join("app/controllers").join("#{grid_controller_class_name.underscore}.rb")
+ end
+
+ def grid_controller_short_name
+ controller_file_name
+ end
+
+ def grid_model_name
+ file_name.camelize.singularize
+ end
+
+ def grid_param_name
+ grid_class_name.underscore
+ end
+
+ def pagination_helper_code
+ if will_paginate?
+ "will_paginate(@grid.assets)"
+ elsif pagy?
+ "pagy_nav(@pagy)"
+ else
+ # Kaminari is default
+ "paginate(@grid.assets)"
+ end
+ end
+
+ def table_helper_code
+ if pagy?
+ "datagrid_table @grid, @records"
+ else
+ "datagrid_table @grid"
+ end
+ end
+
+ def base_grid_file
+ "app/grids/application_grid.rb"
+ end
+
+ def grid_route_name
+ "#{controller_class_name.underscore.gsub('/', '_')}_path"
+ end
+
+ def index_code
+ if pagy?
+ <<-RUBY
+ @grid = #{grid_class_name}.new(grid_params)
+ @pagy, @assets = pagy(@grid.assets)
+ RUBY
+ else
+ <<-RUBY
+ @grid = #{grid_class_name}.new(grid_params) do |scope|
+ scope.page(params[:page])
+ end
+ RUBY
+ end
+ end
+
+ def controller_code
+ <<~RUBY
+ class #{grid_controller_class_name} < ApplicationController
+ def index
+ #{index_code.rstrip}
+ end
+
+ protected
+
+ def grid_params
+ params.fetch(:#{grid_param_name}, {}).permit!
+ end
+ end
+ RUBY
+ end
+
+ def view_code
+ <<~ERB
+ <%= datagrid_form_with model: @grid, url: #{grid_route_name} %>
+
+ <%= #{pagination_helper_code} %>
+ <%= #{table_helper_code} %>
+ <%= #{pagination_helper_code} %>
+ ERB
+ end
+
+ protected
+
+ def generate_routing_namespace(code)
+ depth = regular_class_path.length
+ # Create 'namespace' ladder
+ # namespace :foo do
+ # namespace :bar do
+ namespace_ladder = regular_class_path.each_with_index.map do |ns, i|
+ indent("namespace :#{ns} do\n", i * 2)
+ end.join
+
+ # Create route
+ # get 'baz/index'
+ route = indent(code, depth * 2)
+
+ # Create `end` ladder
+ # end
+ # end
+ end_ladder = (1..depth).reverse_each.map do |i|
+ indent("end\n", i * 2)
+ end.join
+
+ # Combine the 3 parts to generate complete route entry
+ "#{namespace_ladder}#{route}\n#{end_ladder}"
+ end
+
+ def file_exists?(name)
+ name = Rails.root.join(name) unless name.to_s.first == "/"
+ File.exist?(name)
+ end
+
+ def pagy?
+ defined?(::Pagy)
+ end
+
+ def will_paginate?
+ defined?(::WillPaginate)
+ end
+
+ def kaminari?
+ defined?(::Kaminari)
+ end
+ end
+ end
+end
diff --git a/lib/datagrid/generators/views.rb b/lib/datagrid/generators/views.rb
new file mode 100644
index 00000000..3f00a351
--- /dev/null
+++ b/lib/datagrid/generators/views.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Datagrid
+ module Generators
+ class Views < Rails::Generators::Base
+ source_root File.expand_path("../../../app/views/datagrid", __dir__)
+
+ desc "Copies Datagrid partials to your application."
+ def copy_views
+ Dir.glob(File.join(self.class.source_root, "**", "*")).each do |file_path|
+ relative_path = file_path.sub("#{self.class.source_root}/", "")
+
+ next if relative_path == "_order_for.html.erb"
+
+ copy_file(relative_path, File.join("app/views/datagrid", relative_path))
+ end
+ end
+ end
+ end
+end
diff --git a/lib/datagrid/helper.rb b/lib/datagrid/helper.rb
index 24c25eb7..5a1bc374 100644
--- a/lib/datagrid/helper.rb
+++ b/lib/datagrid/helper.rb
@@ -17,7 +17,8 @@ module Datagrid
# [built-in CSS](https://github.com/bogdan/datagrid/blob/master/app/assets/stylesheets/datagrid.sass).
#
# Datagrid includes helpers and a form builder for easy frontend generation.
- # If you need a fully-featured custom GUI, create your templates manually with the help of the {Datagrid::Columns} API.
+ # If you need a fully-featured custom GUI, create your templates manually
+ # with the help of the {Datagrid::Columns} API.
#
# ## Controller and Routing
#
@@ -57,16 +58,17 @@ module Datagrid
#
# Use the built-in partial:
#
- # = datagrid_form_for @grid, url: report_path, other_form_for_option: value
+ # = datagrid_form_with model: @grid, url: report_path, other_form_for_option: value
#
- # {#datagrid_form_for} supports the same options as Rails `form_for`.
+ # {#datagrid_form_with} supports the same options as Rails `form_with`.
#
# ### Advanced Method
#
- # You can use Rails built-in tools to create a form. Additionally, Datagrid provides helpers to generate input/select elements for filters:
+ # You can use Rails built-in tools to create a form.
+ # Additionally, Datagrid provides helpers to generate input/select elements for filters:
#
# ``` haml
- # - form_for UserGrid.new, method: :get, url: users_path do |f|
+ # - form_with model: UserGrid.new, method: :get, url: users_path do |f|
# %div
# = f.datagrid_label :name
# = f.datagrid_filter :name # =>
@@ -152,7 +154,7 @@ module Datagrid
#
# Modify the form for AJAX:
#
- # = datagrid_form_for @grid, html: {class: 'js-datagrid-form'}
+ # = datagrid_form_with model: @grid, html: {class: 'js-datagrid-form'}
# .js-datagrid-table
# = datagrid_table @grid
# .js-pagination
@@ -175,9 +177,8 @@ module Datagrid
#
# app/views/datagrid/
# ├── _enum_checkboxes.html.erb # datagrid_filter for filter(name, :enum, checkboxes: true)
- # ├── _form.html.erb # datagrid_form_for
+ # ├── _form.html.erb # datagrid_form_with
# ├── _head.html.erb # datagrid_header
- # ├── _order_for.html.erb # datagrid_order_for
# ├── _range_filter.html.erb # datagrid_filter for filter(name, type, range: true)
# ├── _row.html.erb # datagrid_rows/datagrid_rows
# └── _table.html.erb # datagrid_table
@@ -194,18 +195,14 @@ module Datagrid
# category.orders.sum(:subtotal) / category.orders.count
# end
#
- # The `:description` option is not built into Datagrid, but you can implement it by modifying the column header
- # partial `app/views/datagrid/_header.html.erb` like this:
+ # The `:description` option is not built into Datagrid, but you can implement it
+ # by adding the following to partial `app/views/datagrid/_header.html.erb`:
#
- # %tr
- # - grid.html_columns(*options[:columns]).each do |column|
- # %th{class: datagrid_column_classes(grid, column)}
- # = column.header
- # - if column.options[:description]
- # %a{data: {toggle: 'tooltip', title: column.options[:description]}}
- # %i.icon-question-sign
- # - if column.order && options[:order]
- # = datagrid_order_for(grid, column, options)
+ # <% if column.options[:description] %>
+ #
+ #
+ #
+ # <% end %>
#
# This modification allows the `:description` tooltip to work with your chosen UI and JavaScript library.
# The same technique can be applied to filters by calling `filter.options` in corresponding partials.
@@ -226,8 +223,7 @@ module Datagrid
#
# This allows you to define a custom `row_class` method in your grid class, like this:
#
- # class IssuesGrid
- # include Datagrid
+ # class IssuesGrid < ApplicationGrid
# scope { Issue }
#
# def row_class(issue)
@@ -246,7 +242,7 @@ module Datagrid
#
# https://github.com/bogdan/datagrid/blob/master/lib/datagrid/locale/en.yml
module Helper
- # @param grid [Datagrid] grid object
+ # @param grid [Datagrid::Base] grid object
# @param column [Datagrid::Columns::Column, String, Symbol] column name
# @param model [Object] an object from grid scope
# @return [Object] individual cell value from the given grid, column name and model
@@ -257,7 +253,9 @@ module Helper
# <% end %>
#
def datagrid_value(grid, column, model)
- datagrid_renderer.format_value(grid, column, model)
+ column = grid.column_by_name(column) if column.is_a?(String) || column.is_a?(Symbol)
+
+ grid.html_value(column, self, model)
end
# @!visibility private
@@ -268,7 +266,7 @@ def datagrid_format_value(grid, column, model)
# Renders html table with columns defined in grid class.
# In the most common used you need to pass paginated collection
# to datagrid table because datagrid do not have pagination compatibilities:
- # @param grid [Datagrid] grid object
+ # @param grid [Datagrid::Base] grid object
# @param assets [Array] objects from grid scope
# @param [Hash{Symbol => Object}] options HTML attributes to be passed to `` tag
# @option options [Hash] html A hash of attributes for the `` tag.
@@ -283,7 +281,14 @@ def datagrid_format_value(grid, column, model)
# assets = grid.assets.page(params[:page])
# datagrid_table(grid, assets, options)
def datagrid_table(grid, assets = grid.assets, **options)
- datagrid_renderer.table(grid, assets, **options)
+ _render_partial(
+ "table", options[:partials],
+ {
+ grid: grid,
+ options: options,
+ assets: assets,
+ },
+ )
end
# Renders HTML table header for given grid instance using columns defined in it
@@ -295,11 +300,19 @@ def datagrid_table(grid, assets = grid.assets, **options)
# Default: all defined columns.
# @option options [String] partials The path for partials lookup.
# Default: `'datagrid'`.
- # @param grid [Datagrid] grid object
+ # @param grid [Datagrid::Base] grid object
+ # @param [Object] opts (deprecated) pass keyword arguments instead
# @param [Hash] options
# @return [String] HTML table header tag markup
- def datagrid_header(grid, options = {})
- datagrid_renderer.header(grid, options)
+ def datagrid_header(grid, opts = :__unspecified__, **options)
+ unless opts == :__unspecified__
+ Datagrid::Utils.warn_once("datagrid_header now requires ** operator when passing options.")
+ options.reverse_merge!(opts)
+ end
+ options[:order] = true unless options.key?(:order)
+
+ _render_partial("head", options[:partials],
+ { grid: grid, options: options },)
end
# Renders HTML table rows using given grid definition using columns defined in it.
@@ -318,34 +331,78 @@ def datagrid_header(grid, options = {})
# %tr
# %td= row.project_name
# %td.project-status{class: row.status}= row.status
+ # @param [Datagrid::Base] grid datagrid object
+ # @param [Array] assets assets as per defined in grid scope
def datagrid_rows(grid, assets = grid.assets, **options, &block)
- datagrid_renderer.rows(grid, assets, **options, &block)
+ safe_join(
+ assets.map do |asset|
+ datagrid_row(grid, asset, **options, &block)
+ end.to_a,
+ )
end
# @return [String] renders ordering controls for the given column name
# @option options [String] partials The path for partials lookup.
# Default: `'datagrid'`.
+ # @param [Datagrid::Base] grid datagrid object
+ # @param [Datagrid::Columns::Column] column
+ # @deprecated Put necessary code inline inside datagrid/head partial.
+ # See built-in partial for example.
def datagrid_order_for(grid, column, options = {})
- datagrid_renderer.order_for(grid, column, options)
+ Datagrid::Utils.warn_once(<<~MSG)
+ datagrid_order_for is deprecated.
+ Put necessary code inline inside datagrid/head partial.
+ See built-in partial for example.
+ MSG
+ _render_partial("order_for", options[:partials],
+ { grid: grid, column: column },)
+ end
+
+ # Renders HTML for grid with all filters inputs and labels defined in it
+ # @option options [String] partials Path for form partial lookup.
+ # Default: `'datagrid'`, which uses `app/views/datagrid/` partials.
+ # Example: `'datagrid_admin'` uses `app/views/datagrid_admin` partials.
+ # @option options [Datagrid::Base] model a Datagrid object to be rendered.
+ # @option options [Hash] All options supported by Rails `form_with` helper.
+ # @param [Hash{Symbol => Object}] options
+ # @return [String] form HTML tag markup
+ def datagrid_form_with(**options)
+ raise ArgumentError, "datagrid_form_with block argument is invalid. Use form_with instead." if block_given?
+
+ grid = options[:model]
+ raise ArgumentError, "Grid has no available filters" if grid&.filters&.empty?
+
+ _render_partial("form", options[:partials], { grid: options[:model], options: options })
end
- # Renders HTML for for grid with all filters inputs and lables defined in it
+ # Renders HTML for grid with all filters inputs and labels defined in it
#
# Supported options:
#
# * :partials - Path for form partial lookup.
# Default: 'datagrid'.
- # * All options supported by Rails form_for helper
- # @param grid [Datagrid] grid object
+ # * All options supported by Rails form_with helper
+ # @deprecated Use {#datagrid_form_with} instead.
+ # @param grid [Datagrid::Base] grid object
# @param [Hash] options
# @return [String] form HTML tag markup
def datagrid_form_for(grid, options = {})
- datagrid_renderer.form_for(grid, options)
+ Datagrid::Utils.warn_once("datagrid_form_for is deprecated if favor of datagrid_form_with.")
+ _render_partial(
+ "form", options[:partials],
+ grid: grid,
+ options: {
+ method: :get,
+ as: grid.param_name,
+ local: true,
+ **options,
+ },
+ )
end
# Provides access to datagrid columns data.
# Used in case you want to build html table completelly manually
- # @param grid [Datagrid] grid object
+ # @param grid [Datagrid::Base] grid object
# @param asset [Object] object from grid scope
# @param block [Proc] block with Datagrid::Helper::HtmlRow as an argument returning a HTML markup as a String
# @param [Hash{Symbol => Object}] options
@@ -364,29 +421,105 @@ def datagrid_form_for(grid, options = {})
# First Name: <%= row.first_name %>
# Last Name: <%= row.last_name %>
def datagrid_row(grid, asset, **options, &block)
- datagrid_renderer.row(grid, asset, **options, &block)
+ Datagrid::Helper::HtmlRow.new(self, grid, asset, options).tap do |row|
+ return capture(row, &block) if block_given?
+ end
end
# Generates an ascending or descending order url for the given column
- # @param grid [Datagrid] grid object
+ # @param grid [Datagrid::Base] grid object
# @param column [Datagrid::Columns::Column, String, Symbol] column name
# @param descending [Boolean] order direction, descending if true, otherwise ascending.
# @return [String] order layout HTML markup
def datagrid_order_path(grid, column, descending)
- datagrid_renderer.order_path(grid, column, descending, request)
- end
-
- protected
-
- def datagrid_renderer
- Renderer.for(self)
+ column = grid.column_by_name(column)
+ query = request&.query_parameters || {}
+ ActionDispatch::Http::URL.path_for(
+ path: request&.path || "/",
+ params: query.merge(grid.query_params(order: column.name, descending: descending)),
+ )
end
+ # @!visibility private
def datagrid_column_classes(grid, column)
+ Datagrid::Utils.warn_once(<<~MSG)
+ datagrid_column_classes is deprecated. Assign necessary classes manually.
+ Correspond to default datagrid/rows partial for example.)
+ MSG
+ column = grid.column_by_name(column)
order_class = if grid.ordered_by?(column)
["ordered", grid.descending ? "desc" : "asc"]
end
- [column.name, order_class, column.options[:class]].compact.join(" ")
+ class_names(column.name, order_class, column.options[:class], column.tag_options[:class])
+ end
+
+ protected
+
+ def _render_partial(partial_name, partials_path, locals = {})
+ render({
+ partial: File.join(partials_path || "datagrid", partial_name),
+ locals: locals,
+ })
+ end
+
+ # Represents a datagrid row that provides access to column values for the given asset
+ # @example
+ # row = datagrid_row(grid, user)
+ # row.class # => Datagrid::Helper::HtmlRow
+ # row.first_name # => "Bogdan "
+ # row.grid # => Datagrid::Base object
+ # row.asset # => User object
+ # row.each do |value|
+ # puts value
+ # end
+ class HtmlRow
+ include Enumerable
+
+ attr_reader :grid, :asset, :options
+
+ # @!visibility private
+ def initialize(renderer, grid, asset, options)
+ @renderer = renderer
+ @grid = grid
+ @asset = asset
+ @options = options
+ end
+
+ # @return [Object] a column value for given column name
+ def get(column)
+ @renderer.datagrid_value(@grid, column, @asset)
+ end
+
+ # Iterates over all column values that are available in the row
+ # param block [Proc] column value iterator
+ def each(&block)
+ (@options[:columns] || @grid.html_columns).each do |column|
+ block.call(get(column))
+ end
+ end
+
+ # @return [String] HTML row format
+ def to_s
+ @renderer.send(:_render_partial, "row", options[:partials], {
+ grid: grid,
+ options: options,
+ asset: asset,
+ },)
+ end
+
+ protected
+
+ def method_missing(method, *args, &blk)
+ if (column = @grid.column_by_name(method))
+ get(column)
+ else
+ super
+ end
+ end
+
+ def respond_to_missing?(method, include_private = false)
+ !!@grid.column_by_name(method) || super
+ end
end
end
end
diff --git a/lib/datagrid/ordering.rb b/lib/datagrid/ordering.rb
index 4619dacc..8b8ebc27 100644
--- a/lib/datagrid/ordering.rb
+++ b/lib/datagrid/ordering.rb
@@ -7,6 +7,7 @@ module Datagrid
class OrderUnsupported < StandardError
end
+ # Module adds support for ordering by defined columns for Datagrid.
module Ordering
# @!visibility private
def self.included(base)
@@ -51,9 +52,11 @@ def order_column
end
# @param column [String, Datagrid::Columns::Column]
+ # @param desc [nil, Boolean] confirm order direction as well if specified
# @return [Boolean] true if given grid is ordered by given column.
- def ordered_by?(column)
- order_column == column_by_name(column)
+ def ordered_by?(column, desc = nil)
+ order_column == column_by_name(column) &&
+ (desc.nil? || (desc ? descending? : !descending?))
end
private
diff --git a/lib/datagrid/renderer.rb b/lib/datagrid/renderer.rb
deleted file mode 100644
index 0e7c1db5..00000000
--- a/lib/datagrid/renderer.rb
+++ /dev/null
@@ -1,154 +0,0 @@
-# frozen_string_literal: true
-
-require "action_view"
-
-module Datagrid
- # @!visibility private
- class Renderer
- def self.for(template)
- new(template)
- end
-
- def initialize(template)
- @template = template
- end
-
- def format_value(grid, column, asset)
- column = grid.column_by_name(column) if column.is_a?(String) || column.is_a?(Symbol)
-
- value = grid.html_value(column, @template, asset)
-
- url = column.options[:url]&.call(asset)
- if url
- @template.link_to(value, url)
- else
- value
- end
- end
-
- def form_for(grid, options = {})
- options[:method] ||= :get
- options[:html] ||= {}
- options[:html][:class] ||= "datagrid-form #{@template.dom_class(grid)}"
- options[:as] ||= grid.param_name
- _render_partial("form", options[:partials], { grid: grid, options: options })
- end
-
- def table(grid, assets, **options)
- options[:html] ||= {}
- options[:html][:class] ||= "datagrid #{@template.dom_class(grid)}"
-
- _render_partial("table", options[:partials],
- {
- grid: grid,
- options: options,
- assets: assets,
- },)
- end
-
- def header(grid, options = {})
- options[:order] = true unless options.key?(:order)
-
- _render_partial("head", options[:partials],
- { grid: grid, options: options },)
- end
-
- def rows(grid, assets = grid.assets, **options, &block)
- result = assets.map do |asset|
- row(grid, asset, **options, &block)
- end.to_a.join
-
- _safe(result)
- end
-
- def row(grid, asset, **options, &block)
- Datagrid::Helper::HtmlRow.new(self, grid, asset, options).tap do |row|
- return @template.capture(row, &block) if block_given?
- end
- end
-
- def order_for(grid, column, options = {})
- _render_partial("order_for", options[:partials],
- { grid: grid, column: column },)
- end
-
- def order_path(grid, column, descending, request)
- column = grid.column_by_name(column)
- query = request ? request.query_parameters : {}
- ActionDispatch::Http::URL.path_for(
- path: request ? request.path : "/",
- params: query.merge(grid.query_params(order: column.name, descending: descending)),
- )
- end
-
- private
-
- def _safe(string)
- string.respond_to?(:html_safe) ? string.html_safe : string
- end
-
- def _render_partial(partial_name, partials_path, locals = {})
- @template.render({
- partial: File.join(partials_path || "datagrid", partial_name),
- locals: locals,
- })
- end
- end
-
- module Helper
- # Represents a datagrid row that provides access to column values for the given asset
- # @example
- # row = datagrid_row(grid, user)
- # row.class # => Datagrid::Helper::HtmlRow
- # row.first_name # => "Bogdan "
- # row.grid # => Grid object
- # row.asset # => User object
- # row.each do |value|
- # puts value
- # end
- class HtmlRow
- include Enumerable
-
- attr_reader :grid, :asset, :options
-
- # @!visibility private
- def initialize(renderer, grid, asset, options)
- @renderer = renderer
- @grid = grid
- @asset = asset
- @options = options
- end
-
- # @return [Object] a column value for given column name
- def get(column)
- @renderer.format_value(@grid, column, @asset)
- end
-
- # Iterates over all column values that are available in the row
- # param block [Proc] column value iterator
- def each(&block)
- (@options[:columns] || @grid.html_columns).each do |column|
- block.call(get(column))
- end
- end
-
- def to_s
- @renderer.send(:_render_partial, "row", options[:partials], {
- grid: grid,
- options: options,
- asset: asset,
- },)
- end
-
- protected
-
- def method_missing(method, *args, &blk)
- if (column = @grid.column_by_name(method))
- get(column)
- else
- super
- end
- end
- end
- end
-end
diff --git a/lib/datagrid/scaffold.rb b/lib/datagrid/scaffold.rb
deleted file mode 100644
index b6937cc6..00000000
--- a/lib/datagrid/scaffold.rb
+++ /dev/null
@@ -1,130 +0,0 @@
-# frozen_string_literal: true
-
-require "rails/generators"
-
-module Datagrid
- # @!visibility private
- class Scaffold < Rails::Generators::NamedBase
- include Rails::Generators::ResourceHelpers
-
- check_class_collision suffix: "Grid"
- source_root File.expand_path("/Users/bogdan/makabu/my/datagrid/lib/datagrid/scaffold.rb/../../../templates")
-
- def create_scaffold
- template "base.rb.erb", base_grid_file unless file_exists?(base_grid_file)
- template "grid.rb.erb", "app/grids/#{grid_class_name.underscore}.rb"
- if file_exists?(grid_controller_file)
- inject_into_file grid_controller_file, index_action, after: %r{class .*#{grid_controller_class_name}.*\n}
- else
- template "controller.rb.erb", grid_controller_file
- end
- template "index.html.erb", view_file
- route(generate_routing_namespace("resources :#{grid_controller_short_name}"))
- gem "kaminari" unless defined?(::Kaminari) || defined?(::WillPaginate)
- in_root do
- {
- "css" => " *= require datagrid",
- "css.sass" => " *= require datagrid",
- "css.scss" => " *= require datagrid",
- }.each do |extension, string|
- file = "app/assets/stylesheets/application.#{extension}"
- if file_exists?(file)
- inject_into_file file, "#{string}\n", { before: %r{.*require_self} } # before all
- end
- end
- end
- end
-
- def view_file
- Rails.root.join("app/views").join(controller_file_path).join("index.html.erb")
- end
-
- def grid_class_name
- "#{file_name.camelize.pluralize}Grid"
- end
-
- def grid_controller_class_name
- "#{controller_class_name.camelize}Controller"
- end
-
- def grid_controller_file
- Rails.root.join("app/controllers").join("#{grid_controller_class_name.underscore}.rb")
- end
-
- def grid_controller_short_name
- controller_file_name
- end
-
- def grid_model_name
- file_name.camelize.singularize
- end
-
- def grid_param_name
- grid_class_name.underscore
- end
-
- def pagination_helper_code
- if defined?(::WillPaginate)
- "will_paginate(@grid.assets)"
- else
- # Kaminari is default
- "paginate(@grid.assets)"
- end
- end
-
- def base_grid_file
- "app/grids/base_grid.rb"
- end
-
- def grid_route_name
- "#{controller_class_name.underscore.gsub('/', '_')}_path"
- end
-
- def index_action
- indent(<<~RUBY)
- def index
- @grid = #{grid_class_name}.new(grid_params) do |scope|
- scope.page(params[:page])
- end
- end
-
- protected
-
- def grid_params
- params.fetch(:#{grid_param_name}, {}).permit!
- end
- RUBY
- end
-
- protected
-
- def generate_routing_namespace(code)
- depth = regular_class_path.length
- # Create 'namespace' ladder
- # namespace :foo do
- # namespace :bar do
- namespace_ladder = regular_class_path.each_with_index.map do |ns, i|
- indent("namespace :#{ns} do\n", i * 2)
- end.join
-
- # Create route
- # get 'baz/index'
- route = indent(code, depth * 2)
-
- # Create `end` ladder
- # end
- # end
- end_ladder = (1..depth).reverse_each.map do |i|
- indent("end\n", i * 2)
- end.join
-
- # Combine the 3 parts to generate complete route entry
- "#{namespace_ladder}#{route}\n#{end_ladder}"
- end
-
- def file_exists?(name)
- name = Rails.root.join(name) unless name.to_s.first == "/"
- File.exist?(name)
- end
- end
-end
diff --git a/lib/datagrid/utils.rb b/lib/datagrid/utils.rb
index e1142ae4..c7ea68d2 100644
--- a/lib/datagrid/utils.rb
+++ b/lib/datagrid/utils.rb
@@ -23,12 +23,20 @@ def translate_from_namespace(namespace, grid_class, key)
I18n.t(lookups.shift, default: lookups).presence
end
+ def deprecator
+ if defined?(Rails) && Rails.version >= "7.1.0"
+ Rails.deprecator
+ else
+ ActiveSupport::Deprecation
+ end
+ end
+
def warn_once(message, delay = 5)
@warnings ||= {}
timestamp = @warnings[message]
return false if timestamp && timestamp >= Time.now - delay
- warn message
+ deprecator.warn(message)
@warnings[message] = Time.now
true
end
@@ -93,6 +101,7 @@ def parse_date(value)
def parse_datetime(value)
return nil if value.blank?
return value if value.is_a?(Range)
+ return value if defined?(ActiveSupport::TimeWithZone) && value.is_a?(ActiveSupport::TimeWithZone)
if value.is_a?(String)
Array(Datagrid.configuration.datetime_formats).each do |format|
@@ -112,10 +121,8 @@ def parse_datetime(value)
def format_date_as_timestamp(value)
if !value
value
- elsif value.is_a?(Array)
- [value.first&.beginning_of_day, value.last&.end_of_day]
elsif value.is_a?(Range)
- (value.begin&.beginning_of_day..value.end&.end_of_day)
+ value.begin&.beginning_of_day..value.end&.end_of_day
else
value.beginning_of_day..value.end_of_day
end
diff --git a/lib/datagrid/version.rb b/lib/datagrid/version.rb
index 12d0cd3f..403ac57e 100644
--- a/lib/datagrid/version.rb
+++ b/lib/datagrid/version.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
module Datagrid
- VERSION = "1.8.4"
+ VERSION = "2.0.0-alpha"
end
diff --git a/lib/tasks/datagrid_tasks.rake b/lib/tasks/datagrid_tasks.rake
deleted file mode 100644
index e1ca194a..00000000
--- a/lib/tasks/datagrid_tasks.rake
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-namespace :datagrid do
- desc "Copy table partials into rails application"
- task :copy_partials do
- require "fileutils"
- views_path = "app/views/datagrid"
- destination_dir = (Rails.root + views_path).to_s
- pattern = "#{File.expand_path(File.dirname(__FILE__) + "/../../#{views_path}")}/*"
- Dir[pattern].each do |template|
- puts "* copy #{template} => #{destination_dir}"
- FileUtils.mkdir_p destination_dir
- FileUtils.cp template, destination_dir
- end
- end
-end
diff --git a/spec/datagrid/columns/column_spec.rb b/spec/datagrid/columns/column_spec.rb
index 6594eeb3..12bc2b17 100644
--- a/spec/datagrid/columns/column_spec.rb
+++ b/spec/datagrid/columns/column_spec.rb
@@ -5,8 +5,7 @@
describe Datagrid::Columns::Column do
describe ".inspect" do
subject do
- class ColumnInspectTest
- include Datagrid
+ class ColumnInspectTest < Datagrid::Base
scope { Entry }
column(:id, header: "ID")
end
@@ -14,7 +13,7 @@ class ColumnInspectTest
end
it "shows inspect information" do
- expect(subject.inspect).to eq("#\"ID\"}>")
+ expect(subject.inspect).to eq('#"ID"}>')
end
end
end
diff --git a/spec/datagrid/columns_spec.rb b/spec/datagrid/columns_spec.rb
index 97dd01f2..57c6b26c 100644
--- a/spec/datagrid/columns_spec.rb
+++ b/spec/datagrid/columns_spec.rb
@@ -65,8 +65,7 @@
describe "translations" do
module ::Ns45
- class TranslatedReport
- include Datagrid
+ class TranslatedReport < Datagrid::Base
scope { Entry }
column(:name)
end
@@ -78,8 +77,7 @@ class TranslatedReport
end
it "translates column-header without namespace" do
- class Report27
- include Datagrid
+ class Report27 < Datagrid::Base
scope { Entry }
column(:name)
end
@@ -90,8 +88,7 @@ class Report27
end
it "translates column-header in using defaults namespace" do
- class Report27
- include Datagrid
+ class Report27 < Datagrid::Base
scope { Entry }
column(:name)
end
@@ -148,7 +145,9 @@ class Report27
end
it "should support csv export" do
- expect(subject.to_csv).to eq("Shipping date,Group,Name,Access level,Pet\n#{date},Pop,Star,admin,ROTTWEILER\n")
+ expect(subject.to_csv).to eq(
+ "Shipping date,Group,Name,Access level,Pet\n#{date},Pop,Star,admin,ROTTWEILER\n",
+ )
end
it "should support csv export of particular columns" do
@@ -156,7 +155,9 @@ class Report27
end
it "should support csv export options" do
- expect(subject.to_csv(col_sep: ";")).to eq("Shipping date;Group;Name;Access level;Pet\n#{date};Pop;Star;admin;ROTTWEILER\n")
+ expect(subject.to_csv(col_sep: ";")).to eq(
+ "Shipping date;Group;Name;Access level;Pet\n#{date};Pop;Star;admin;ROTTWEILER\n",
+ )
end
end
@@ -178,8 +179,7 @@ class Report27
end
it "should inherit columns correctly" do
- parent = Class.new do
- include Datagrid
+ parent = Class.new(Datagrid::Base) do
scope { Entry }
column(:name)
end
@@ -209,7 +209,7 @@ class Report27
filter(:name)
column(:entries_count, "count(entries.id)") do |model|
format("(#{model.entries_count})") do |value|
- content_tag(:span, value)
+ tag.span(value)
end
end
end
@@ -579,8 +579,7 @@ def capitalized_name
describe "#data_hash" do
it "works" do
pending
- class DataHashGrid
- include Datagrid
+ class DataHashGrid < Datagrid::Base
scope { Entry }
column(:name, order: true)
end
diff --git a/spec/datagrid/core_spec.rb b/spec/datagrid/core_spec.rb
index 0b0b5628..58cae7e7 100644
--- a/spec/datagrid/core_spec.rb
+++ b/spec/datagrid/core_spec.rb
@@ -24,8 +24,7 @@
before { 2.times { Entry.create } }
let(:report_class) do
- class ScopeTestReport
- include Datagrid
+ class ScopeTestReport < Datagrid::Base
scope { Entry.order("id desc") }
end
ScopeTestReport
@@ -95,21 +94,21 @@ class TestGrid < ScopeTestReport
describe "#inspect" do
it "should show all attribute values" do
- class InspectTest
- include Datagrid
+ class InspectTest < Datagrid::Base
scope { Entry }
filter(:created_at, :date, range: true)
column(:name)
end
grid = InspectTest.new(created_at: %w[2014-01-01 2014-08-05], descending: true, order: "name")
- expect(grid.inspect).to eq("#")
+ expect(grid.inspect).to eq(
+ "#",
+ )
end
end
describe "#==" do
- class EqualTest
- include Datagrid
+ class EqualTest < Datagrid::Base
scope { Entry }
filter(:created_at, :date)
column(:name)
@@ -203,13 +202,14 @@ class EqualTest
end
it "permites all attributes by default" do
- expect do
- test_grid(params) do
- scope { Entry }
- filter(:name)
- end
- end.to_not raise_error
+ grid = test_grid(params) do
+ scope { Entry }
+ filter(:name)
+ end
+
+ expect(grid.name).to eq("one")
end
+
it "doesn't permit attributes when forbidden_attributes_protection is set" do
expect do
test_grid(params) do
@@ -219,6 +219,7 @@ class EqualTest
end
end.to raise_error(ActiveModel::ForbiddenAttributesError)
end
+
it "permits attributes when forbidden_attributes_protection is set and attributes are permitted" do
expect do
test_grid(params.permit!) do
@@ -228,6 +229,13 @@ class EqualTest
end
end.to_not raise_error
end
+
+ it "supports hash attribute assignment" do
+ grid = test_grid_filter(:group_id, :integer, range: true)
+ grid.attributes = ActionController::Parameters.new(group_id: { from: 1, to: 2 })
+
+ expect(grid.group_id).to eq(1..2)
+ end
end
describe ".query_param" do
diff --git a/spec/datagrid/drivers/active_record_spec.rb b/spec/datagrid/drivers/active_record_spec.rb
index 5b84de20..d8a802cd 100644
--- a/spec/datagrid/drivers/active_record_spec.rb
+++ b/spec/datagrid/drivers/active_record_spec.rb
@@ -18,8 +18,10 @@
end
it "should support append_column_queries" do
- scope = subject.append_column_queries(Entry.where({}),
- [Datagrid::Columns::Column.new(test_grid_class, :sum_group_id, "sum(entries.group_id)")],)
+ scope = subject.append_column_queries(
+ Entry.where({}),
+ [Datagrid::Columns::Column.new(test_grid_class, :sum_group_id, "sum(entries.group_id)")],
+ )
expect(scope.to_sql.strip).to eq('SELECT "entries".*, sum(entries.group_id) AS sum_group_id FROM "entries"')
end
@@ -36,13 +38,13 @@
end
end
- describe "gotcha #datagrid_where_by_timestamp" do
+ describe "where by timestamp" do
subject do
test_grid(created_at: 10.days.ago..5.days.ago) do
scope { Entry }
filter(:created_at, :date, range: true) do |value, scope, _grid|
- scope.joins(:group).datagrid_where_by_timestamp("groups.created_at", value)
+ scope.joins(:group).where(groups: { created_at: value })
end
end.assets
end
diff --git a/spec/datagrid/drivers/array_spec.rb b/spec/datagrid/drivers/array_spec.rb
index 3e708b29..50fb0ad2 100644
--- a/spec/datagrid/drivers/array_spec.rb
+++ b/spec/datagrid/drivers/array_spec.rb
@@ -12,9 +12,8 @@
end
describe "api" do
- class ArrayGrid
+ class ArrayGrid < Datagrid::Base
User = Struct.new(:name, :age)
- include Datagrid
scope do
[]
end
@@ -102,8 +101,7 @@ class ArrayGrid
end
describe "array of hashes" do
- class HashGrid
- include Datagrid
+ class HashGrid < Datagrid::Base
scope do
[{ name: "Bogdan", age: 30 }, { name: "Brad", age: 32 }]
end
diff --git a/spec/datagrid/drivers/sequel_spec.rb b/spec/datagrid/drivers/sequel_spec.rb
index 60db3336..225491f3 100644
--- a/spec/datagrid/drivers/sequel_spec.rb
+++ b/spec/datagrid/drivers/sequel_spec.rb
@@ -33,8 +33,7 @@
end
it "supports pagination" do
- class PaginationTest
- include Datagrid
+ class PaginationTest < Datagrid::Base
scope { SequelEntry }
end
grid = PaginationTest.new do |scope|
diff --git a/spec/datagrid/filters/base_filter_spec.rb b/spec/datagrid/filters/base_filter_spec.rb
index 09c88f38..cb3c84de 100644
--- a/spec/datagrid/filters/base_filter_spec.rb
+++ b/spec/datagrid/filters/base_filter_spec.rb
@@ -15,4 +15,19 @@ def name_default
expect(report.assets).not_to include(Entry.create!(name: "world"))
expect(report.assets).not_to include(Entry.create!(name: ""))
end
+
+ describe "#default_scope?" do
+ it "identifies filters without custom block" do
+ grid = test_grid do
+ scope { Entry }
+ filter(:id, :integer)
+ filter(:group_id, :integer) do |value, _scope|
+ scope("group_id >= ?", value)
+ end
+ end
+
+ expect(grid.filter_by_name(:id)).to be_default_scope
+ expect(grid.filter_by_name(:group_id)).to_not be_default_scope
+ end
+ end
end
diff --git a/spec/datagrid/filters/composite_filters_spec.rb b/spec/datagrid/filters/composite_filters_spec.rb
deleted file mode 100644
index c5a221f4..00000000
--- a/spec/datagrid/filters/composite_filters_spec.rb
+++ /dev/null
@@ -1,76 +0,0 @@
-# frozen_string_literal: true
-
-require "spec_helper"
-
-describe Datagrid::Filters::CompositeFilters do
- describe ".date_range_filters" do
- it "should generate from date and to date filters" do
- e1 = Entry.create!(shipping_date: 6.days.ago)
- e2 = Entry.create!(shipping_date: 4.days.ago)
- e3 = Entry.create!(shipping_date: 1.days.ago)
- assets = test_grid(from_shipping_date: 5.days.ago, to_shipping_date: 2.day.ago) do
- scope { Entry }
- silence_warnings do
- date_range_filters(:shipping_date)
- end
- end.assets
-
- expect(assets).to include(e2)
- expect(assets).not_to include(e1, e3)
- end
-
- it "should support options" do
- report = test_grid do
- silence_warnings do
- date_range_filters(:shipping_date, { default: 10.days.ago.to_date }, { default: Date.today })
- end
- end
- expect(report.from_shipping_date).to eq(10.days.ago.to_date)
- expect(report.to_shipping_date).to eq(Date.today)
- end
- it "should support table name in field" do
- report = test_grid do
- silence_warnings do
- date_range_filters("entries.shipping_date", { default: 10.days.ago.to_date }, { default: Date.today })
- end
- end
- expect(report.from_entries_shipping_date).to eq(10.days.ago.to_date)
- expect(report.to_entries_shipping_date).to eq(Date.today)
- end
- end
-
- describe ".integer_range_filters" do
- it "should generate from integer and to integer filters" do
- e1 = Entry.create!(group_id: 1)
- e2 = Entry.create!(group_id: 3)
- e3 = Entry.create!(group_id: 5)
- assets = test_grid(from_group_id: 2, to_group_id: 4) do
- scope { Entry }
- silence_warnings do
- integer_range_filters(:group_id)
- end
- end.assets
-
- expect(assets).to include(e2)
- expect(assets).not_to include(e1, e3)
- end
- it "should support options" do
- report = test_grid do
- silence_warnings do
- integer_range_filters(:group_id, { default: 0 }, { default: 100 })
- end
- end
- expect(report.from_group_id).to eq(0)
- expect(report.to_group_id).to eq(100)
- end
- it "should table name in field name" do
- report = test_grid do
- silence_warnings do
- integer_range_filters("entries.group_id", { default: 0 }, { default: 100 })
- end
- end
- expect(report.from_entries_group_id).to eq(0)
- expect(report.to_entries_group_id).to eq(100)
- end
- end
-end
diff --git a/spec/datagrid/filters/date_filter_spec.rb b/spec/datagrid/filters/date_filter_spec.rb
index 4a21becd..60744b77 100644
--- a/spec/datagrid/filters/date_filter_spec.rb
+++ b/spec/datagrid/filters/date_filter_spec.rb
@@ -7,21 +7,33 @@
it "supports date range argument" do
e1 = Entry.create!(created_at: 7.days.ago)
e2 = Entry.create!(created_at: 4.days.ago)
- e3 = Entry.create!(created_at: 1.day.ago)
+ e3 = Entry.create!(created_at: 3.days.ago)
+ e4 = Entry.create!(created_at: 1.day.ago)
- report = test_grid_filter(:created_at, :date)
+ report = test_grid_filter(:created_at, :date, range: true)
report.created_at = 5.day.ago..3.days.ago
+ expect(report.created_at).to eq(5.days.ago.to_date..3.days.ago.to_date)
expect(report.assets).not_to include(e1)
expect(report.assets).to include(e2)
- expect(report.assets).not_to include(e3)
+ expect(report.assets).to include(e3)
+ expect(report.assets).not_to include(e4)
+ end
+
+ it "raises when range assigned to non-range filter" do
+ expect do
+ test_grid(created_at: 5.day.ago..3.days.ago) do
+ scope { Entry }
+ filter(:created_at, :date)
+ end
+ end.to raise_error(ArgumentError)
end
it "endless date range argument" do
e1 = Entry.create!(created_at: 7.days.ago)
e2 = Entry.create!(created_at: 4.days.ago)
- report = test_grid_filter(:created_at, :date)
+ report = test_grid_filter(:created_at, :date, range: true)
report.created_at = (5.days.ago..)
expect(report.assets).not_to include(e1)
@@ -31,6 +43,32 @@
expect(report.assets).not_to include(e2)
end
+ it "supports hash argument for range filter" do
+ report = test_grid_filter(:created_at, :date, range: true)
+ from = 5.days.ago
+ to = 3.days.ago
+ report.created_at = { from: from, to: to }
+ expect(report.created_at).to eq(from.to_date..to.to_date)
+
+ report.created_at = { "from" => from, "to" => to }
+ expect(report.created_at).to eq(from.to_date..to.to_date)
+
+ report.created_at = {}
+ expect(report.created_at).to eq(nil)
+
+ report.created_at = { from: nil, to: nil }
+ expect(report.created_at).to eq(nil)
+
+ report.created_at = { from: Date.today, to: nil }
+ expect(report.created_at).to eq(Date.today..nil)
+
+ report.created_at = { from: nil, to: Date.today }
+ expect(report.created_at).to eq(nil..Date.today)
+
+ report.created_at = { from: Time.now, to: Time.now }
+ expect(report.created_at).to eq(Date.today..Date.today)
+ end
+
{ active_record: Entry, mongoid: MongoidEntry, sequel: SequelEntry }.each do |orm, klass|
describe "with orm #{orm}", orm => true do
describe "date to timestamp conversion" do
@@ -121,9 +159,11 @@ def entry_dated(date)
e1 = Entry.create!(created_at: 7.days.ago)
e2 = Entry.create!(created_at: 4.days.ago)
e3 = Entry.create!(created_at: 1.day.ago)
+
report = test_grid_filter(:created_at, :date, range: true)
report.created_at = range
- expect(report.created_at).to eq([range.last.to_date, range.first.to_date])
+
+ expect(report.created_at).to eq(range.last.to_date..range.first.to_date)
expect(report.assets).to include(e1)
expect(report.assets).to include(e2)
expect(report.assets).to include(e3)
@@ -132,10 +172,12 @@ def entry_dated(date)
it "should support block" do
date = Date.new(2018, 0o1, 0o7)
time = Time.utc(2018, 0o1, 0o7, 2, 2)
+
report = test_grid_filter(:created_at, :date, range: true) do |value|
- where("created_at >= ?", value)
+ where(created_at: value)
end
report.created_at = date
+
expect(report.assets).not_to include(Entry.create!(created_at: time - 1.day))
expect(report.assets).to include(Entry.create!(created_at: time))
end
@@ -166,7 +208,7 @@ def entry_dated(date)
report = test_grid_filter(:created_at, :date, range: true)
report.created_at = %w[2013-01-01 2012-01-01]
- expect(report.created_at).to eq([Date.new(2012, 0o1, 0o1), Date.new(2013, 0o1, 0o1)])
+ expect(report.created_at).to eq(Date.new(2012, 0o1, 0o1)..Date.new(2013, 0o1, 0o1))
end
it "should nullify blank range" do
@@ -185,6 +227,14 @@ def entry_dated(date)
end
end
+ it "deserializes range" do
+ report = test_grid_filter(:created_at, :date, range: true)
+
+ value = Date.new(2012, 1, 1)..Date.new(2012, 1, 2)
+ report.created_at = value.as_json
+ expect(report.created_at).to eq(value)
+ end
+
it "supports search by timestamp column" do
report = test_grid_filter(:created_at, :date)
report.created_at = Date.today
@@ -200,4 +250,18 @@ def entry_dated(date)
expect(report.assets).to include(e4)
expect(report.assets).to_not include(e5)
end
+
+ it "allows filter to be defined before scope" do
+ class ParentGrid < Datagrid::Base
+ filter(:created_at, :date, range: true)
+ end
+
+ class ChildGrid < ParentGrid
+ scope do
+ Entry
+ end
+ end
+
+ expect(ChildGrid.new.assets).to eq([])
+ end
end
diff --git a/spec/datagrid/filters/date_time_filter_spec.rb b/spec/datagrid/filters/date_time_filter_spec.rb
index 3da1c18a..990c1be7 100644
--- a/spec/datagrid/filters/date_time_filter_spec.rb
+++ b/spec/datagrid/filters/date_time_filter_spec.rb
@@ -121,14 +121,14 @@ def entry_dated(date)
report = test_grid_filter(:created_at, :datetime, range: true)
report.created_at = range
- expect(report.created_at).to eq([range.last, range.first])
+ expect(report.created_at).to eq(range.last..range.first)
expect(report.assets).to include(e1)
expect(report.assets).to include(e2)
expect(report.assets).to include(e3)
end
it "should support block" do
- report = test_grid_filter(:created_at, :datetime, range: true) do |value|
+ report = test_grid_filter(:created_at, :datetime) do |value|
where("created_at >= ?", value)
end
report.created_at = Time.now
@@ -162,6 +162,30 @@ def entry_dated(date)
report = test_grid_filter(:created_at, :datetime, range: true)
report.created_at = ["2013-01-01 01:00", "2012-01-01 01:00"]
- expect(report.created_at).to eq([Time.new(2012, 0o1, 0o1, 1, 0), Time.new(2013, 0o1, 0o1, 1, 0)])
+ expect(report.created_at).to eq(Time.new(2012, 0o1, 0o1, 1, 0)..Time.new(2013, 0o1, 0o1, 1, 0))
+ end
+
+ it "supports serialized range value" do
+ from = Time.parse("2013-01-01 01:00")
+ to = Time.parse("2013-01-02 02:00")
+ report = test_grid_filter(:created_at, :datetime, range: true)
+
+ report.created_at = (from..to).as_json
+ expect(report.created_at).to eq(from..to)
+
+ report.created_at = (from..).as_json
+ expect(report.created_at).to eq(from..)
+
+ report.created_at = (..to).as_json
+ expect(report.created_at).to eq(..to)
+
+ report.created_at = (from...to).as_json
+ expect(report.created_at).to eq(from...to)
+
+ report.created_at = (nil..nil).as_json
+ expect(report.created_at).to eq(nil)
+
+ report.created_at = (nil...nil).as_json
+ expect(report.created_at).to eq(nil)
end
end
diff --git a/spec/datagrid/filters/dynamic_filter_spec.rb b/spec/datagrid/filters/dynamic_filter_spec.rb
index 3ad793c8..42ffcff3 100644
--- a/spec/datagrid/filters/dynamic_filter_spec.rb
+++ b/spec/datagrid/filters/dynamic_filter_spec.rb
@@ -59,22 +59,26 @@
it "should nullify incorrect value for integer" do
report.condition = [:group_id, "<=", "aa"]
- expect(report.condition).to eq([:group_id, "<=", nil])
+ expect(report.condition.to_h).to eq(
+ { field: :group_id, operation: "<=", value: nil },
+ )
end
it "should nullify incorrect value for date" do
report.condition = [:shipping_date, "<=", "aa"]
- expect(report.condition).to eq([:shipping_date, "<=", nil])
+ expect(report.condition.to_h).to eq({
+ field: :shipping_date, operation: "<=", value: nil,
+ })
end
it "should nullify incorrect value for datetime" do
report.condition = [:created_at, "<=", "aa"]
- expect(report.condition).to eq([:created_at, "<=", nil])
+ expect(report.condition.to_h).to eq({ field: :created_at, operation: "<=", value: nil })
end
it "should support date comparation operation by timestamp column" do
report.condition = [:created_at, "<=", "1986-08-05"]
- expect(report.condition).to eq([:created_at, "<=", Date.parse("1986-08-05")])
+ expect(report.condition.to_h).to eq({ field: :created_at, operation: "<=", value: Date.parse("1986-08-05") })
expect(report.assets).to include(Entry.create!(created_at: Time.parse("1986-08-04 01:01:01")))
expect(report.assets).to include(Entry.create!(created_at: Time.parse("1986-08-05 23:59:59")))
expect(report.assets).to include(Entry.create!(created_at: Time.parse("1986-08-05 00:00:00")))
@@ -84,7 +88,7 @@
it "should support date = operation by timestamp column" do
report.condition = [:created_at, "=", "1986-08-05"]
- expect(report.condition).to eq([:created_at, "=", Date.parse("1986-08-05")])
+ expect(report.condition.to_h).to eq({ field: :created_at, operation: "=", value: Date.parse("1986-08-05") })
expect(report.assets).not_to include(Entry.create!(created_at: Time.parse("1986-08-04 23:59:59")))
expect(report.assets).to include(Entry.create!(created_at: Time.parse("1986-08-05 23:59:59")))
expect(report.assets).to include(Entry.create!(created_at: Time.parse("1986-08-05 00:00:01")))
@@ -95,7 +99,7 @@
it "should support date =~ operation by timestamp column" do
report.condition = [:created_at, "=~", "1986-08-05"]
- expect(report.condition).to eq([:created_at, "=~", Date.parse("1986-08-05")])
+ expect(report.condition.to_h).to eq({ field: :created_at, operation: "=~", value: Date.parse("1986-08-05") })
expect(report.assets).not_to include(Entry.create!(created_at: Time.parse("1986-08-04 23:59:59")))
expect(report.assets).to include(Entry.create!(created_at: Time.parse("1986-08-05 23:59:59")))
expect(report.assets).to include(Entry.create!(created_at: Time.parse("1986-08-05 00:00:01")))
@@ -144,9 +148,9 @@
scope { Entry }
filter(
:condition, :dynamic, operations: ["=", "!="],
- ) do |(field, operation, value), scope|
- if operation == "!="
- scope.where("#{field} != ?", value)
+ ) do |filter, scope|
+ if filter.operation == "!="
+ scope.where("#{filter.field} != ?", filter.value)
else
default_filter
end
@@ -169,4 +173,16 @@
report.assets
end.to raise_error(Datagrid::FilteringError)
end
+
+ it "supports assignment of string keys hash" do
+ report.condition = {
+ field: "shipping_date",
+ operation: "<>",
+ value: "1996-08-05",
+ }.stringify_keys
+
+ expect(report.condition.to_h).to eq({
+ field: "shipping_date", operation: "<>", value: Date.parse("1996-08-05"),
+ })
+ end
end
diff --git a/spec/datagrid/filters/enum_filter_spec.rb b/spec/datagrid/filters/enum_filter_spec.rb
index ac398e18..c8d44d07 100644
--- a/spec/datagrid/filters/enum_filter_spec.rb
+++ b/spec/datagrid/filters/enum_filter_spec.rb
@@ -24,8 +24,7 @@
end
it "should initialize select option only on instanciation" do
- class ReportWithLazySelect
- include Datagrid
+ class ReportWithLazySelect < Datagrid::Base
scope { Entry }
filter(:group_id, :enum, select: proc { raise "hello" })
end
diff --git a/spec/datagrid/filters/integer_filter_spec.rb b/spec/datagrid/filters/integer_filter_spec.rb
index 1757860e..c5b76fb9 100644
--- a/spec/datagrid/filters/integer_filter_spec.rb
+++ b/spec/datagrid/filters/integer_filter_spec.rb
@@ -11,7 +11,7 @@
let(:entry7) { Entry.create!(group_id: 7) }
it "should support integer range argument" do
- report = test_grid_filter(:group_id, :integer)
+ report = test_grid_filter(:group_id, :integer, range: true)
report.group_id = 3..5
expect(report.assets).not_to include(entry1)
@@ -30,47 +30,44 @@
it "should support integer range given as array argument" do
report = test_grid_filter(:group_id, :integer, range: true)
report.group_id = [3.to_s, 5.to_s]
-
- expect(report.assets).not_to include(entry7)
- expect(report.assets).to include(entry4)
- expect(report.assets).not_to include(entry1)
+ expect(report.group_id).to eq(3..5)
end
it "should support minimum integer argument" do
report = test_grid_filter(:group_id, :integer, range: true)
report.group_id = [5.to_s, nil]
- expect(report.assets).not_to include(entry1)
- expect(report.assets).not_to include(entry4)
- expect(report.assets).to include(entry7)
+ expect(report.group_id).to eq(5..)
end
it "should support maximum integer argument" do
report = test_grid_filter(:group_id, :integer, range: true)
report.group_id = [nil, 5.to_s]
- expect(report.assets).to include(entry1)
- expect(report.assets).to include(entry4)
- expect(report.assets).not_to include(entry7)
+ expect(report.group_id).to eq(..5)
end
it "should find something in one integer interval" do
report = test_grid_filter(:group_id, :integer, range: true)
- report.group_id = (4..4)
+ report.group_id = 4
expect(report.assets).not_to include(entry7)
expect(report.assets).to include(entry4)
expect(report.assets).not_to include(entry1)
end
- it "should support invalid range" do
+ it "supports range inversion" do
report = test_grid_filter(:group_id, :integer, range: true)
report.group_id = 7..1
- expect(report.group_id).to eq([1, 7])
- expect(report.assets).to include(entry7)
- expect(report.assets).to include(entry4)
- expect(report.assets).to include(entry1)
+ expect(report.group_id).to eq(1..7)
+ end
+
+ it "converts infinite range to nil" do
+ report = test_grid_filter(:group_id, :integer, range: true)
+ report.group_id = nil..nil
+
+ expect(report.group_id).to eq(nil)
end
it "should support block" do
@@ -88,7 +85,7 @@
scope { Entry.joins(:group) }
filter(:rating, :integer, range: true)
end
- expect(report.rating).to eq([4, nil])
+ expect(report.rating).to eq(4..nil)
expect(report.assets).not_to include(Entry.create!(group: Group.create!(rating: 3)))
expect(report.assets).to include(Entry.create!(group: Group.create!(rating: 5)))
end
@@ -133,4 +130,57 @@
expect(report.group_id).to eq(group.id)
end
+
+ it "supports serialized range value" do
+ report = test_grid_filter(:group_id, :integer, range: true)
+
+ report.group_id = (1..5).as_json
+ expect(report.group_id).to eq(1..5)
+
+ report.group_id = (1..).as_json
+ expect(report.group_id).to eq(1..)
+
+ report.group_id = (..5).as_json
+ expect(report.group_id).to eq(..5)
+
+ report.group_id = (1...5).as_json
+ expect(report.group_id).to eq(1...5)
+
+ report.group_id = (nil..nil).as_json
+ expect(report.group_id).to eq(nil)
+
+ report.group_id = (nil...nil).as_json
+ expect(report.group_id).to eq(nil)
+ end
+
+ it "type casts value" do
+ report = test_grid_filter(:group_id, :integer)
+
+ report.group_id = "1"
+ expect(report.group_id).to eq(1)
+
+ report.group_id = " 1 "
+ expect(report.group_id).to eq(1)
+
+ report.group_id = 1.1
+ expect(report.group_id).to eq(1)
+
+ report.group_id = "1.1"
+ expect(report.group_id).to eq(1)
+
+ report.group_id = "-1"
+ expect(report.group_id).to eq(-1)
+
+ report.group_id = "-1.1"
+ expect(report.group_id).to eq(-1)
+
+ report.group_id = "1a"
+ expect(report.group_id).to eq(1)
+
+ report.group_id = "aa"
+ expect(report.group_id).to eq(nil)
+
+ report.group_id = "a1"
+ expect(report.group_id).to eq(nil)
+ end
end
diff --git a/spec/datagrid/filters_spec.rb b/spec/datagrid/filters_spec.rb
index b5431616..3534503f 100644
--- a/spec/datagrid/filters_spec.rb
+++ b/spec/datagrid/filters_spec.rb
@@ -40,9 +40,7 @@
it "should initialize when report Scope table not exists" do
class ModelWithoutTable < ActiveRecord::Base; end
expect(ModelWithoutTable).not_to be_table_exists
- class TheReport
- include Datagrid
-
+ class TheReport < Datagrid::Base
scope { ModelWithoutTable }
filter(:name)
@@ -52,8 +50,7 @@ class TheReport
end
it "should support inheritence" do
- parent = Class.new do
- include Datagrid
+ parent = Class.new(Datagrid::Base) do
scope { Entry }
filter(:name)
end
@@ -175,8 +172,7 @@ def check_performed(value, result, **options)
describe "tranlations" do
module ::Ns46
- class TranslatedReport
- include Datagrid
+ class TranslatedReport < Datagrid::Base
scope { Entry }
filter(:name)
end
@@ -239,8 +235,7 @@ class InheritedReport < TranslatedReport
describe "#inspect" do
it "should list all fitlers with types" do
module ::NsInspect
- class TestGrid
- include Datagrid
+ class TestGrid < Datagrid::Base
scope { Entry }
filter(:id, :integer)
filter(:name, :string)
@@ -254,8 +249,7 @@ class TestGrid
end
it "dislays no filters" do
- class TestGrid8728
- include Datagrid
+ class TestGrid8728 < Datagrid::Base
scope { Entry }
end
diff --git a/spec/datagrid/form_builder_spec.rb b/spec/datagrid/form_builder_spec.rb
index dc5abd01..cdccec7c 100644
--- a/spec/datagrid/form_builder_spec.rb
+++ b/spec/datagrid/form_builder_spec.rb
@@ -16,16 +16,17 @@ class MyTemplate
action_view_template
end
- let(:view) { ActionView::Helpers::FormBuilder.new(:report, _grid, template, view_options) }
+ let(:view) do
+ ActionView::Helpers::FormBuilder.new(
+ :report, _grid, template,
+ skip_default_ids: false,
+ **view_options,
+ )
+ end
+
let(:view_options) { {} }
describe ".datagrid_filter" do
- it "should work for every filter type" do
- Datagrid::Filters::FILTER_TYPES.each_value do |klass|
- expect(Datagrid::FormBuilder.instance_methods.map(&:to_sym)).to include(klass.form_builder_helper_name)
- end
- end
-
subject do
view.datagrid_filter(_filter, **_filter_options, &_filter_block)
end
@@ -41,7 +42,7 @@ class MyTemplate
let(:_filter) { :name }
it {
should equal_to_dom(<<~HTML)
-
+
HTML
}
end
@@ -54,7 +55,7 @@ class MyTemplate
it {
should equal_to_dom(<<~HTML)
-
+
HTML
}
@@ -63,7 +64,7 @@ class MyTemplate
it {
should equal_to_dom(<<~HTML)
-
+
HTML
}
end
@@ -77,7 +78,7 @@ class MyTemplate
it {
should equal_to_dom(<<~HTML)
-
+
HTML
}
@@ -91,51 +92,54 @@ class MyTemplate
it {
should equal_to_dom(<<~HTML)
-
+
HTML
}
end
end
context "with input_options" do
- context "type is date" do
+ context "date filter type is text" do
let(:_filter) { :created_at }
let(:_grid) do
- test_grid_filter(:created_at, :date, input_options: { type: :date })
+ test_grid_filter(:created_at, :date, input_options: { type: "text" })
end
it {
should equal_to_dom(<<~HTML)
-
+
HTML
}
end
- context "type is textarea" do
+ context "string filter type is textarea" do
let(:_filter) { :name }
let(:_grid) do
test_grid_filter(:name, :string, input_options: { type: :textarea })
end
it {
- should equal_to_dom(<<~HTML)
-
- HTML
+ should equal_to_dom(
+ '',
+ )
}
end
- context "type is datetime-local" do
+ context "datetime filter type is text" do
let(:_filter) { :created_at }
let(:_grid) do
- test_grid(created_at: Time.new(2024, 1, 1, 9, 25, 15)) do
+ created_at = ActiveSupport::TimeZone["UTC"].local(
+ 2024, 1, 1, 9, 25, 15,
+ )
+ test_grid(created_at: created_at) do
scope { Entry }
- filter(:created_at, :datetime, input_options: { type: "datetime-local" })
+ filter(:created_at, :datetime, input_options: { type: "text" })
end
end
it {
should equal_to_dom(<<~HTML)
-
+
HTML
}
@@ -146,13 +150,13 @@ class MyTemplate
it {
should equal_to_dom(<<~HTML)
-
+
HTML
}
end
end
- context "type is date" do
+ context "datetime filter type is date" do
let(:_filter) { :created_at }
let(:_grid) do
test_grid(created_at: Date.new(2024, 1, 1)) do
@@ -162,9 +166,11 @@ class MyTemplate
end
it {
- should equal_to_dom(<<~HTML)
-
- HTML
+ should equal_to_dom(
+ <<~HTML,
+
+ HTML
+ )
}
end
end
@@ -182,11 +188,13 @@ class MyTemplate
let(:_range) { [1, 2] }
it {
- should equal_to_dom(<<~HTML)
-
- -
-
- HTML
+ should equal_to_dom(
+ ' ' \
+ ' - ' \
+ ' ',
+ )
}
end
@@ -195,9 +203,9 @@ class MyTemplate
it {
should equal_to_dom(<<~HTML)
-
- -
-
+ \
+ -
+
HTML
}
it { should be_html_safe }
@@ -208,9 +216,9 @@ class MyTemplate
it {
should equal_to_dom(<<~HTML)
-
- -
-
+
+ -
+
HTML
}
it { should be_html_safe }
@@ -221,9 +229,9 @@ class MyTemplate
it {
should equal_to_dom(<<~HTML)
-
- -
-
+
+ -
+
HTML
}
end
@@ -242,9 +250,11 @@ class MyTemplate
let(:view_options) { { partials: "not_existed" } }
let(:_range) { nil }
it {
- should equal_to_dom(
- ' - ',
- )
+ should equal_to_dom(<<~HTML)
+
+ -
+
+ HTML
}
end
end
@@ -261,9 +271,9 @@ class MyTemplate
it {
should equal_to_dom(<<~HTML)
-
- -
-
+
+ -
+
HTML
}
end
@@ -282,9 +292,9 @@ class MyTemplate
it {
should equal_to_dom(<<~HTML)
-
- -
-
+
+ -
+
HTML
}
it { should be_html_safe }
@@ -300,9 +310,9 @@ class MyTemplate
it {
should equal_to_dom(<<~HTML)
-
- -
-
+
+ -
+
HTML
}
end
@@ -312,9 +322,9 @@ class MyTemplate
it {
should equal_to_dom(<<~HTML)
-
- -
-
+
+ -
+
HTML
}
it { should be_html_safe }
@@ -324,9 +334,9 @@ class MyTemplate
let(:_range) { Date.parse("2012-01-02")..Date.parse("2012-01-01") }
it {
should equal_to_dom(<<~HTML)
-
- -
-
+
+ -
+
HTML
}
end
@@ -340,9 +350,9 @@ class MyTemplate
let(:_range) { [nil, nil] }
it {
should equal_to_dom(<<~HTML)
-
- -
-
+
+ -
+
HTML
}
end
@@ -357,7 +367,7 @@ class MyTemplate
it {
should equal_to_dom(<<~HTML)
-
+
first
second
@@ -368,13 +378,13 @@ class MyTemplate
context "when block is given" do
let(:_filter_block) do
proc do
- template.content_tag(:option, "block option", value: "block_value")
+ template.tag.option("block option", value: "block_value")
end
end
it {
should equal_to_dom(<<~HTML)
-
+
block option
@@ -389,7 +399,7 @@ class MyTemplate
it {
should equal_to_dom(<<~HTML)
-
+
first
second
@@ -403,7 +413,7 @@ class MyTemplate
it {
should equal_to_dom(<<~HTML)
-
+
first
second
@@ -416,7 +426,7 @@ class MyTemplate
it {
should equal_to_dom(<<~HTML)
-
+
Choose plz
first
second
@@ -430,7 +440,7 @@ class MyTemplate
it {
should equal_to_dom(<<~HTML)
-
+
My Prompt
first
second
@@ -444,7 +454,7 @@ class MyTemplate
it {
should equal_to_dom(<<~HTML)
-
+
first
second
@@ -457,20 +467,61 @@ class MyTemplate
let(:_category_filter_options) { { checkboxes: true } }
it {
- should equal_to_dom(<<~HTML)
-
- first
-
-
- second
-
- HTML
+ should equal_to_dom(
+ <<~HTML,
+
+
+
+ first
+
+
+
+ second
+
+
+ HTML
+ )
}
+ it "disables label[for] attribute" do
+ expect(view.datagrid_label(_filter)).to eq("Category ")
+ end
+
context "when partials option passed and partial exists" do
let(:view_options) { { partials: "custom_checkboxes" } }
it { should equal_to_dom("custom_enum_checkboxes") }
end
+
+ context "when using deprecated elements variable in partial" do
+ around do |ex|
+ Datagrid::Utils.deprecator.silence do
+ ex.run
+ end
+ end
+ let(:view_options) { { partials: "deprecated_enum_checkboxes" } }
+ it {
+ should equal_to_dom(
+ [["first", "first", false], ["second", "second", false]].to_json,
+ )
+ }
+ end
+
+ context "when inline class attribute specified" do
+ let(:_filter_options) { { for: nil, class: "custom-class" } }
+
+ it { should equal_to_dom(<<~HTML) }
+
+
+ first
+
+
+ second
+
+
+ HTML
+ end
end
end
@@ -481,9 +532,10 @@ class MyTemplate
end
it {
+ # hidden is important when default is set to true
should equal_to_dom(<<~HTML)
-
+
HTML
}
end
@@ -494,7 +546,7 @@ class MyTemplate
end
it {
should equal_to_dom(
- %(
+ %(
Yes
No ),
@@ -508,11 +560,7 @@ class MyTemplate
let(:_filter) { :name }
- it {
- should equal_to_dom(
- ' ',
- )
- }
+ it { should equal_to_dom(' ') }
context "when multiple option is set" do
let(:_grid) do
@@ -526,7 +574,7 @@ class MyTemplate
it {
should equal_to_dom(
- ' ',
+ ' ',
)
}
end
@@ -542,7 +590,7 @@ class MyTemplate
)
end
let(:_filter) { :name }
- it { should equal_to_dom(' ') }
+ it { should equal_to_dom(' ') }
end
context "with float filter type" do
let(:_grid) do
@@ -551,7 +599,7 @@ class MyTemplate
let(:_filter) { :group_id }
it {
should equal_to_dom(
- ' ',
+ ' ',
)
}
end
@@ -563,7 +611,7 @@ class MyTemplate
let(:_filter) { :group_id }
let(:expected_html) do
<<~HTML
-
+
hello
HTML
end
@@ -586,7 +634,7 @@ class MyTemplate
let(:_filter) { :column_names }
let(:expected_html) do
<<~HTML
- Id
+ Id
Name
Category
HTML
@@ -609,9 +657,20 @@ class MyTemplate
let(:_filter) { :column_names }
let(:expected_html) do
<<~DOM
- Id
- Name
- Category
+
+
+
+ Id
+
+
+
+ Name
+
+
+
+ Category
+
+
DOM
end
@@ -633,7 +692,7 @@ class MyTemplate
context "with no options" do
let(:expected_html) do
<<-HTML
- Id
+ Id
Group
Name
Category
@@ -643,10 +702,10 @@ class MyTemplate
Confirmed
Shipping date
Created at
- Updated at =
+ Updated at =
≈
≥
- ≤
+ ≤
HTML
end
it { should equal_to_dom(expected_html) }
@@ -657,13 +716,19 @@ class MyTemplate
end
let(:expected_html) do
<<-HTML
- id
- name =
- ≈
- ≥
- ≤
+
+ id
+ name
+
+
+ =
+ ≈
+ ≥
+ ≤
+
HTML
end
+
it { should equal_to_dom(expected_html) }
end
@@ -673,8 +738,8 @@ class MyTemplate
end
let(:expected_html) do
<<-HTML
- id name ≥
- ≤
+ id name ≥
+ ≤
HTML
end
it { should equal_to_dom(expected_html) }
@@ -686,8 +751,8 @@ class MyTemplate
end
let(:expected_html) do
<<-HTML
- ≥
- ≤
+ ≥
+ ≤
HTML
end
it { should equal_to_dom(expected_html) }
@@ -698,7 +763,7 @@ class MyTemplate
end
let(:expected_html) do
<<-HTML
- id name
+ id name
HTML
end
it { should equal_to_dom(expected_html) }
diff --git a/spec/datagrid/generators/scaffold_spec.rb b/spec/datagrid/generators/scaffold_spec.rb
new file mode 100644
index 00000000..843b187f
--- /dev/null
+++ b/spec/datagrid/generators/scaffold_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+describe Datagrid::Generators::Scaffold do
+ subject { Datagrid::Generators::Scaffold.new(["user"]) }
+
+ describe ".pagination_helper_code" do
+ it "uses kaminari by default" do
+ expect(subject.pagination_helper_code).to eql("paginate(@grid.assets)")
+ end
+
+ context "when WillPaginate exists" do
+ before(:each) do
+ Object.const_set("WillPaginate", 1)
+ end
+ it "uses willpaginate" do
+ expect(subject.pagination_helper_code).to eql("will_paginate(@grid.assets)")
+ end
+
+ after(:each) do
+ Object.send(:remove_const, "WillPaginate")
+ end
+ end
+ end
+
+ describe "#controller_code" do
+ it "works" do
+ expect(subject.controller_code).to eq(<<~RUBY)
+ class UsersController < ApplicationController
+ def index
+ @grid = UsersGrid.new(grid_params) do |scope|
+ scope.page(params[:page])
+ end
+ end
+
+ protected
+
+ def grid_params
+ params.fetch(:users_grid, {}).permit!
+ end
+ end
+ RUBY
+ end
+
+ context "with pagy" do
+ before do
+ allow(subject).to receive(:pagy?).and_return(true)
+ end
+
+ it "works" do
+ expect(subject.controller_code).to eq(<<~RUBY)
+ class UsersController < ApplicationController
+ def index
+ @grid = UsersGrid.new(grid_params)
+ @pagy, @assets = pagy(@grid.assets)
+ end
+
+ protected
+
+ def grid_params
+ params.fetch(:users_grid, {}).permit!
+ end
+ end
+ RUBY
+ end
+ end
+ end
+
+ describe "#view_code" do
+ it "works" do
+ expect(subject.view_code).to eq(<<~ERB)
+ <%= datagrid_form_with model: @grid, url: users_path %>
+
+ <%= paginate(@grid.assets) %>
+ <%= datagrid_table @grid %>
+ <%= paginate(@grid.assets) %>
+ ERB
+ end
+
+ context "with pagy" do
+ before do
+ allow(subject).to receive(:pagy?).and_return(true)
+ end
+
+ it "works" do
+ expect(subject.view_code).to eq(<<~ERB)
+ <%= datagrid_form_with model: @grid, url: users_path %>
+
+ <%= pagy_nav(@pagy) %>
+ <%= datagrid_table @grid, @records %>
+ <%= pagy_nav(@pagy) %>
+ ERB
+ end
+ end
+ end
+end
diff --git a/spec/datagrid/helper_spec.rb b/spec/datagrid/helper_spec.rb
index 6c895693..d99a9e6c 100644
--- a/spec/datagrid/helper_spec.rb
+++ b/spec/datagrid/helper_spec.rb
@@ -5,8 +5,6 @@
require "active_support/core_ext/object"
require "action_controller"
-require "datagrid/renderer"
-
describe Datagrid::Helper do
subject do
action_view_template
@@ -21,6 +19,9 @@
options.is_a?(String) ? options : ["/location", options.to_param.presence].compact.join("?")
end
+ # Rails defaults since 6.1
+ ActionView::Helpers::FormHelper.form_with_generates_ids = true
+ ActionView::Helpers::FormHelper.form_with_generates_remote_forms = false
ActionView::Helpers::FormTagHelper.default_enforce_utf8 = false
end
@@ -44,7 +45,7 @@
datagrid_table = subject.datagrid_table(grid)
expect(datagrid_table).to match_css_pattern(
- "table.datagrid tr td.noresults" => 1,
+ "table.datagrid-table tr td.datagrid-no-results" => 1,
)
expect(datagrid_table).to include(I18n.t("datagrid.no_results"))
end
@@ -53,31 +54,19 @@
describe ".datagrid_table" do
it "should have grid class as html class on table" do
expect(subject.datagrid_table(grid)).to match_css_pattern(
- "table.datagrid.simple_report" => 1,
- )
- end
- it "should have namespaced grid class as html class on table" do
- module ::Ns23
- class TestGrid
- include Datagrid
- scope { Entry }
- column(:id)
- end
- end
- expect(subject.datagrid_table(Ns23::TestGrid.new)).to match_css_pattern(
- "table.datagrid.ns23_test_grid" => 1,
+ "table.datagrid-table" => 1,
)
end
it "should return data table html" do
datagrid_table = subject.datagrid_table(grid)
expect(datagrid_table).to match_css_pattern({
- "table.datagrid tr th.group div.order" => 1,
- "table.datagrid tr th.group" => %r{Group.*},
- "table.datagrid tr th.name div.order" => 1,
- "table.datagrid tr th.name" => %r{Name.*},
- "table.datagrid tr td.group" => "Pop",
- "table.datagrid tr td.name" => "Star",
+ "table.datagrid-table tr th[data-column=group] div.datagrid-order" => 1,
+ "table.datagrid-table tr th[data-column=group]" => %r{Group.*},
+ "table.datagrid-table tr th[data-column=name] div.datagrid-order" => 1,
+ "table.datagrid-table tr th[data-column=name]" => %r{Name.*},
+ "table.datagrid-table tr td[data-column=group]" => "Pop",
+ "table.datagrid-table tr td[data-column=name]" => "Star",
})
end
@@ -86,25 +75,26 @@ class TestGrid
datagrid_table = subject.datagrid_table(grid, [entry])
expect(datagrid_table).to match_css_pattern({
- "table.datagrid tr th.group div.order" => 1,
- "table.datagrid tr th.group" => %r{Group.*},
- "table.datagrid tr th.name div.order" => 1,
- "table.datagrid tr th.name" => %r{Name.*},
- "table.datagrid tr td.group" => "Pop",
- "table.datagrid tr td.name" => "Star",
+ "table.datagrid-table tr th[data-column=group] div.datagrid-order" => 1,
+ "table.datagrid-table tr th[data-column=group]" => %r{Group.*},
+ "table.datagrid-table tr th[data-column=name] div.datagrid-order" => 1,
+ "table.datagrid-table tr th[data-column=name]" => %r{Name.*},
+ "table.datagrid-table tr td[data-column=group]" => "Pop",
+ "table.datagrid-table tr td[data-column=name]" => "Star",
})
end
it "should support no order given" do
- expect(subject.datagrid_table(grid, [entry], order: false)).to match_css_pattern("table.datagrid th .order" => 0)
+ expect(subject.datagrid_table(grid, [entry],
+ order: false,)).to match_css_pattern("table.datagrid-table th .datagrid-order" => 0)
end
it "should support columns option" do
expect(subject.datagrid_table(grid, [entry], columns: [:name])).to match_css_pattern(
- "table.datagrid th.name" => 1,
- "table.datagrid td.name" => 1,
- "table.datagrid th.group" => 0,
- "table.datagrid td.group" => 0,
+ "table.datagrid-table th[data-column=name]" => 1,
+ "table.datagrid-table td[data-column=name]" => 1,
+ "table.datagrid-table th[data-column=group]" => 0,
+ "table.datagrid-table td[data-column=group]" => 0,
)
end
@@ -119,10 +109,10 @@ class TestGrid
it "should output only given column names" do
expect(subject.datagrid_table(grid, [entry])).to match_css_pattern(
- "table.datagrid th.name" => 1,
- "table.datagrid td.name" => 1,
- "table.datagrid th.category" => 0,
- "table.datagrid td.category" => 0,
+ "table.datagrid-table th[data-column=name]" => 1,
+ "table.datagrid-table td[data-column=name]" => 1,
+ "table.datagrid-table th[data-column=category]" => 0,
+ "table.datagrid-table td[data-column=category]" => 0,
)
end
end
@@ -153,7 +143,6 @@ class TestGrid
expect(rendered_partial).to include "Namespaced table partial."
expect(rendered_partial).to include "Namespaced row partial."
expect(rendered_partial).to include "Namespaced head partial."
- expect(rendered_partial).to include "Namespaced order_for partial."
end
end
@@ -168,8 +157,8 @@ class TestGrid
end
it "should render table" do
expect(subject.datagrid_table(grid)).to match_css_pattern(
- "table.datagrid th.name" => 1,
- "table.datagrid td.name" => 2,
+ "table.datagrid-table th[data-column=name]" => 1,
+ "table.datagrid-table td[data-column=name]" => 2,
)
end
end
@@ -184,34 +173,20 @@ class TestGrid
end
it "should render table" do
expect(subject.datagrid_table(grid)).to match_css_pattern(
- "table.datagrid th.name" => 1,
- "table.datagrid td.name" => 2,
+ "table.datagrid-table th[data-column=name]" => 1,
+ "table.datagrid-table td[data-column=name]" => 2,
)
end
end
end
describe ".datagrid_rows" do
- it "should support urls" do
- rp = test_grid_column(:name, url: ->(model) { "/entries/#{model.name.downcase}" })
- expect(subject.datagrid_rows(rp, [entry])).to match_css_pattern(
- "tr td.name a[href='/entries/star']" => "Star",
- )
- end
-
- it "should support conditional urls" do
- rp = test_grid_column(:name, url: ->(_model) { false })
- expect(subject.datagrid_rows(rp, [entry])).to match_css_pattern(
- "tr td.name" => "Star",
- )
- end
-
it "should add ordering classes to column" do
rp = test_grid_column(:name)
rp.order = :name
expect(subject.datagrid_rows(rp, [entry])).to match_css_pattern(
- "tr td.name.ordered.asc" => "Star",
+ "tr td[data-column=name].datagrid-order-active-asc" => "Star",
)
end
it "should add ordering classes to column" do
@@ -220,7 +195,7 @@ class TestGrid
expect(
subject.datagrid_rows(rp) do |row|
- subject.content_tag(:strong, row.name)
+ subject.tag.strong(row.name)
end,
).to match_css_pattern(
"strong" => "Star",
@@ -233,7 +208,7 @@ class TestGrid
rp.descending = true
expect(subject.datagrid_rows(rp, [entry])).to match_css_pattern(
- "tr td.name.ordered.desc" => "Star",
+ "tr td[data-column=name].datagrid-order-active-desc" => "Star",
)
end
@@ -241,16 +216,16 @@ class TestGrid
rp = test_grid_column(:name, &:name)
expect(subject.datagrid_rows(rp, [entry])).to match_css_pattern(
- "tr td.name" => "Star",
+ "tr td[data-column=name]" => "Star",
)
end
it "should render html columns" do
rp = test_grid_column(:name, html: true) do |model|
- content_tag(:span, model.name)
+ tag.span(model.name)
end
expect(subject.datagrid_rows(rp, [entry])).to match_css_pattern(
- "tr td.name span" => "Star",
+ "tr td[data-column=name] span" => "Star",
)
end
@@ -258,7 +233,7 @@ class TestGrid
rp = test_grid_column(:name, html: true, &:name)
expect(subject.datagrid_rows(rp, [entry])).to match_css_pattern(
- "tr td.name" => "Star",
+ "tr td[data-column=name]" => "Star",
)
end
@@ -271,7 +246,7 @@ class TestGrid
end
expect(subject.datagrid_rows(rp, [entry])).to match_css_pattern(
- "tr td.name" => "Star",
+ "tr td[data-column=name]" => "Star",
)
end
@@ -279,41 +254,41 @@ class TestGrid
rp = test_grid_column(:name, html: true, data: "DATA", &:name)
expect(subject.datagrid_rows(rp, [entry])).to match_css_pattern(
- "tr td.name" => "Star",
+ "tr td[data-column=name]" => "Star",
)
end
it "should render argument-based html columns" do
- rp = test_grid_column(:name, html: ->(data) { content_tag :h1, data })
+ rp = test_grid_column(:name, html: ->(data) { tag.h1 data })
expect(subject.datagrid_rows(rp, [entry])).to match_css_pattern(
- "tr td.name h1" => "Star",
+ "tr td[data-column=name] h1" => "Star",
)
end
it "should render argument-based html columns with custom data" do
- rp = test_grid_column(:name, html: ->(data) { content_tag :em, data }) do
+ rp = test_grid_column(:name, html: ->(data) { tag.em data }) do
name.upcase
end
expect(subject.datagrid_rows(rp, [entry])).to match_css_pattern(
- "tr td.name em" => "STAR",
+ "tr td[data-column=name] em" => "STAR",
)
end
it "should render html columns with double arguments for column" do
rp = test_grid_column(:name, html: true) do |model, grid|
- content_tag(:span, "#{model.name}-#{grid.assets.klass}")
+ tag.span("#{model.name}-#{grid.assets.klass}")
end
expect(subject.datagrid_rows(rp, [entry])).to match_css_pattern(
- "tr td.name span" => "Star-Entry",
+ "tr td[data-column=name] span" => "Star-Entry",
)
end
it "should render argument-based html blocks with double arguments" do
rp = test_grid_column(:name, html: lambda { |data, model|
- content_tag :h1, "#{data}-#{model.name.downcase}"
+ tag.h1 "#{data}-#{model.name.downcase}"
},)
expect(subject.datagrid_rows(rp, [entry])).to match_css_pattern(
- "tr td.name h1" => "Star-star",
+ "tr td[data-column=name] h1" => "Star-star",
)
end
@@ -322,7 +297,7 @@ class TestGrid
content_tag :h1, "#{data}-#{model.name.downcase}-#{grid.assets.klass}"
},)
expect(subject.datagrid_rows(rp, [entry])).to match_css_pattern(
- "tr td.name h1" => "Star-star-Entry",
+ "tr td[data-column=name] h1" => "Star-star-Entry",
)
end
@@ -333,7 +308,7 @@ class TestGrid
name.upcase
end
expect(subject.datagrid_rows(rp, [entry])).to match_css_pattern(
- "tr td.name h1" => "STAR-Star",
+ "tr td[data-column=name] h1" => "STAR-Star",
)
end
@@ -347,7 +322,7 @@ class TestGrid
end
end
expect(subject.datagrid_rows(rp, [entry])).to match_css_pattern(
- "tr td.name h1" => "STAR-Star-Entry",
+ "tr td[data-column=name] h1" => "STAR-Star-Entry",
)
end
@@ -358,24 +333,40 @@ class TestGrid
column(:category)
end
expect(subject.datagrid_rows(rp, [entry], columns: [:name])).to match_css_pattern(
- "tr td.name" => "Star",
+ "tr td[data-column=name]" => "Star",
)
expect(subject.datagrid_rows(rp, [entry], columns: [:name])).to match_css_pattern(
- "tr td.category" => 0,
+ "tr td[data-column=category]" => 0,
)
end
it "should allow CSS classes to be specified for a column" do
- rp = test_grid do
- scope { Entry }
- column(:name, class: "my_class")
+ rp = expect_deprecated do
+ test_grid_column(:name, class: "my-class")
end
expect(subject.datagrid_rows(rp, [entry])).to match_css_pattern(
- "tr td.name.my_class" => "Star",
+ "tr td[data-column=name].my-class" => "Star",
)
end
+ it "supports tag_options option" do
+ report = test_grid(order: :name, descending: true) do
+ scope { Entry }
+ column(:name, tag_options: {
+ class: "my-class",
+ "data-column-group": "core",
+ "data-column": nil,
+ },)
+ end
+
+ expect(subject.datagrid_rows(report, [entry])).to equal_to_dom(<<~HTML)
+
+ Star
+
+ HTML
+ end
+
context "when grid has complicated columns" do
let(:grid) do
test_grid(name: "Hello") do
@@ -388,7 +379,7 @@ class TestGrid
end
it "should ignore them" do
expect(subject.datagrid_rows(grid, [entry])).to match_css_pattern(
- "td.name" => 1,
+ "td[data-column=name]" => 1,
)
end
end
@@ -396,7 +387,7 @@ class TestGrid
it "should escape html" do
entry.update!(name: "hello
")
expect(subject.datagrid_rows(grid, [entry], columns: [:name])).to equal_to_dom(<<-HTML)
- <div>hello</div>
+ <div>hello</div>
HTML
end
@@ -406,28 +397,36 @@ class TestGrid
model.name.html_safe
end
expect(subject.datagrid_rows(grid, [entry], columns: [:safe_name])).to equal_to_dom(<<-HTML)
- hello
+ hello
HTML
end
end
describe ".datagrid_order_for" do
it "should render ordering layout" do
- class OrderedGrid
- include Datagrid
+ class OrderedGrid < Datagrid::Base
scope { Entry }
column(:category)
end
object = OrderedGrid.new(descending: true, order: :category)
- expect(subject.datagrid_order_for(object, object.column_by_name(:category))).to equal_to_dom(<<~HTML)
-
- HTML
+ silence_deprecator do
+ expect(subject.datagrid_order_for(object, object.column_by_name(:category))).to equal_to_dom(<<~HTML)
+
+ HTML
+ end
end
end
+
describe ".datagrid_form_for" do
+ around(:each) do |e|
+ silence_deprecator do
+ e.run
+ end
+ end
+
it "returns namespaced partial if partials options is passed" do
rendered_form = subject.datagrid_form_for(grid, {
url: "",
@@ -436,17 +435,17 @@ class OrderedGrid
expect(rendered_form).to include "Namespaced form partial."
end
it "should render form and filter inputs" do
- class FormForGrid
- include Datagrid
+ class FormForGrid < Datagrid::Base
scope { Entry }
- filter(:category)
+ filter(:category, :string)
end
object = FormForGrid.new(category: "hello")
expect(subject.datagrid_form_for(object, url: "/grid")).to equal_to_dom(<<~HTML)
-