Skip to content

Commit

Permalink
Allow configuring the sortable column on Field
Browse files Browse the repository at this point in the history
This takes the sortable field previously explored in thoughtbot#2658.

Here, we allow overriding the sorting column on a field, plus turn off
sorting if it wouldn't make sense for the given data.
  • Loading branch information
goosys authored and nickcharlton committed Feb 17, 2025
1 parent 936da16 commit 1a92f62
Show file tree
Hide file tree
Showing 15 changed files with 278 additions and 41 deletions.
10 changes: 6 additions & 4 deletions app/controllers/administrate/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -141,16 +141,18 @@ def order
@order ||= Administrate::Order.new(
sorting_attribute,
sorting_direction,
association_attribute: order_by_field(
sorting_column: sorting_column(
dashboard_attribute(sorting_attribute)
)
)
end

def order_by_field(dashboard)
return unless dashboard.try(:options)
def sorting_column(dashboard_attribute)
return unless dashboard_attribute.try(:options)

dashboard.options.fetch(:order, nil)
dashboard_attribute.options.fetch(:sorting_column) {
dashboard_attribute.options.fetch(:order, nil)
}
end

def dashboard_attribute(attribute)
Expand Down
46 changes: 28 additions & 18 deletions app/views/administrate/application/_collection.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,35 @@ to display a collection of resources in an HTML table.
<tr>
<% collection_presenter.attribute_types.each do |attr_name, attr_type| %>
<th class="cell-label
cell-label--<%= attr_type.html_class %>
cell-label--<%= collection_presenter.ordered_html_class(attr_name) %>
cell-label--<%= "#{collection_presenter.resource_name}_#{attr_name}" %>"
scope="col"
aria-sort="<%= sort_order(collection_presenter.ordered_html_class(attr_name)) %>">
<%= link_to(sanitized_order_params(page, collection_field_name).merge(
collection_presenter.order_params_for(attr_name, key: collection_field_name)
)) do %>
<%= t(
"helpers.label.#{collection_presenter.resource_name}.#{attr_name}",
default: resource_class.human_attribute_name(attr_name).titleize,
) %>
<% if collection_presenter.ordered_by?(attr_name) %>
<span class="cell-label__sort-indicator cell-label__sort-indicator--<%= collection_presenter.ordered_html_class(attr_name) %>">
<svg aria-hidden="true">
<use xlink:href="#icon-up-caret" />
</svg>
</span>
cell-label--<%= attr_type.html_class %>
cell-label--<%= collection_presenter.ordered_html_class(attr_name) %>
cell-label--<%= "#{collection_presenter.resource_name}_#{attr_name}" %>"
scope="col"
<% if attr_type.sortable? %>
aria-sort="<%= sort_order(collection_presenter.ordered_html_class(attr_name)) %>"
<% end %>
>
<% if attr_type.sortable? %>
<%= link_to(sanitized_order_params(page, collection_field_name).merge(
collection_presenter.order_params_for(attr_name, key: collection_field_name)
)) do %>
<%= t(
"helpers.label.#{collection_presenter.resource_name}.#{attr_name}",
default: resource_class.human_attribute_name(attr_name).titleize,
) %>
<% if collection_presenter.ordered_by?(attr_name) %>
<span class="cell-label__sort-indicator cell-label__sort-indicator--<%= collection_presenter.ordered_html_class(attr_name) %>">
<svg aria-hidden="true">
<use xlink:href="#icon-up-caret" />
</svg>
</span>
<% end %>
<% end %>
<% else %>
<%= t(
"helpers.label.#{collection_presenter.resource_name}.#{attr_name}",
default: resource_class.human_attribute_name(attr_name).titleize,
) %>
<% end %>
</th>
<% end %>
Expand Down
77 changes: 77 additions & 0 deletions docs/customizing_dashboards.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ For example:
with this, you will be able to search through the column `name` from the
association `belongs_to :country`, from your model.

`:sortable` - Specifies if sorting should be enabled in the table views.
Default is `true`.

`:sorting_column` - Specifies the column of the associated model to be used for sorting in the table views and dropdown menu.
If `sorting_column` is omitted and `order` is specified, the value of `order` will be used.
If neither is specified, sorting will be done using the foreign key.

`:class_name` - Specifies the name of the associated class.

`:primary_key` (deprecated) - Specifies the association's primary_key.
Expand All @@ -124,6 +131,14 @@ set this to `0` or `false`. Default is `5`.

`:direction` - What direction the sort should be in, `:asc` (default) or `:desc`.

`:sortable` - Specifies if sorting should be enabled in the table views.
Default is `true`.

`:sorting_column` - Specifies the column of the associated model to be used for sorting in the dropdown menu.
If `sorting_column` is omitted and `sort_by` is specified, the value of `sort_by` will be used.
If neither is specified, sorting will be done using the foreign key.
For `HasMany` associations, sorting is based on the count, so this option is not referenced in the table views.

`:class_name` - Specifies the name of the associated class.

`:primary_key` (deprecated) - Specifies object's primary_key.
Expand Down Expand Up @@ -154,6 +169,13 @@ For example:
with this, you will be able to search through the column `name` from the
association `has_one :city`, from your model.

`:sortable` - Specifies if sorting should be enabled in the table views.
Default is `true`.

`:sorting_column` - Specifies the column of the associated model to be used for sorting in the table views and dropdown menu.
If `sorting_column` is omitted and `order` is specified, the value of `order` will be used.
If neither is specified, sorting will be done using the foreign key.

`:class_name` - Specifies the name of the associated class.

**Field::Number**
Expand All @@ -162,6 +184,13 @@ association `has_one :city`, from your model.
Note that currently number fields are searched like text, which may yield
more results than expected. Default is `false`.

`:sortable` - Specifies if sorting should be enabled in the table views.
Default is `true`.

`:sorting_column` - Specifies the column to be used for sorting in the table view.
By default, the column itself is used, but when used as a virtual column,
a custom sorting column can be specified.

`:decimals` - Set the number of decimals to display. Defaults to `0`.

`:prefix` - Prefixes the number with a string. Defaults to `""`.
Expand Down Expand Up @@ -210,8 +239,18 @@ Default is `[]`.
`:order` - What to sort the association by in the form select.
Default is `nil`.

`:sortable` - Specifies if sorting should be enabled in the table views.
Default is `true`.

**Field::DateTime**

`:sortable` - Specifies if sorting should be enabled in the table views.
Default is `true`.

`:sorting_column` - Specifies the column to be used for sorting in the table view.
By default, the column itself is used, but when used as a virtual column,
a custom sorting column can be specified.

`:format` - Specify what format, using `strftime` you would like `DateTime`
objects to display as.

Expand All @@ -220,6 +259,13 @@ in.

**Field::Date**

`:sortable` - Specifies if sorting should be enabled in the table views.
Default is `true`.

`:sorting_column` - Specifies the column to be used for sorting in the table view.
By default, the column itself is used, but when used as a virtual column,
a custom sorting column can be specified.

`:format` - Specify what format, using `strftime` you would like `Date`
objects to display as.

Expand Down Expand Up @@ -249,13 +295,27 @@ If no collection is provided and no enum can be detected, the list of options wi
`:searchable` - Specify if the attribute should be considered when searching.
Default is `true`.

`:sortable` - Specifies if sorting should be enabled in the table views.
Default is `true`.

`:sorting_column` - Specifies the column to be used for sorting in the table view.
By default, the column itself is used, but when used as a virtual column,
a custom sorting column can be specified.

`:include_blank` - Similar to [the option of the same name accepted by Rails helpers](https://api.rubyonrails.org/classes/ActionView/Helpers/FormOptionsHelper.html). If provided, a "blank" option will be added first to the list of options, with the value of `include_blank` as label.

**Field::String**

`:searchable` - Specify if the attribute should be considered when searching.
Default is `true`.

`:sortable` - Specifies if sorting should be enabled in the table views.
Default is `true`.

`:sorting_column` - Specifies the column to be used for sorting in the table view.
By default, the column itself is used, but when used as a virtual column,
a custom sorting column can be specified.

`:truncate` - Set the number of characters to display in the index view.
Defaults to `50`.

Expand All @@ -264,6 +324,13 @@ Defaults to `50`.
`:searchable` - Specify if the attribute should be considered when searching.
Default is `false`.

`:sortable` - Specifies if sorting should be enabled in the table views.
Default is `true`.

`:sorting_column` - Specifies the column to be used for sorting in the table view.
By default, the column itself is used, but when used as a virtual column,
a custom sorting column can be specified.

`:truncate` - Set the number of characters to display in the index view.
Defaults to `50`.

Expand All @@ -275,6 +342,13 @@ Example: `.with_options(input_options: { rows: 20 })`
`:searchable` - Specify if the attribute should be considered when searching.
Default is `true`.

`:sortable` - Specifies if sorting should be enabled in the table views.
Default is `true`.

`:sorting_column` - Specifies the column to be used for sorting in the table view.
By default, the column itself is used, but when used as a virtual column,
a custom sorting column can be specified.

`:truncate` - Set the number of characters to display in the index view.
Defaults to `50`.

Expand All @@ -286,6 +360,9 @@ Defaults is `{}`.
`:searchable` - Specify if the attribute should be considered when searching.
Default is `false`.

`:sortable` - Specifies if sorting should be enabled in the table views.
Default is `false`.

`:truncate` - Set the number of characters to display in the views.
Defaults to `50`.

Expand Down
4 changes: 4 additions & 0 deletions lib/administrate/field/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ def self.searchable?
false
end

def self.sortable?
true
end

def self.field_type
to_s.split("::").last.underscore
end
Expand Down
4 changes: 4 additions & 0 deletions lib/administrate/field/deferred.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ def searchable_fields
end
end

def sortable?
options.fetch(:sortable, deferred_class.sortable?)
end

def permitted_attribute(attr, opts = {})
if options.key?(:foreign_key)
Administrate.warn_of_deprecated_option(:foreign_key)
Expand Down
23 changes: 21 additions & 2 deletions lib/administrate/field/has_many.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,21 @@ def data
def order_from_params(params)
Administrate::Order.new(
params.fetch(:order, sort_by),
params.fetch(:direction, direction)
params.fetch(:direction, direction),
sorting_column: sorting_column(
associated_dashboard_attribute(params.fetch(:order, sort_by))
)
)
end

def order
@order ||= Administrate::Order.new(sort_by, direction)
@order ||= Administrate::Order.new(
sort_by,
direction,
sorting_column: sorting_column(
associated_dashboard_attribute(sort_by)
)
)
end

private
Expand All @@ -109,6 +118,16 @@ def display_candidate_resource(resource)
associated_dashboard.display_resource(resource)
end

def sorting_column(dashboard_attribute)
return unless dashboard_attribute.try(:options)

dashboard_attribute.options.fetch(:sorting_column, nil)
end

def associated_dashboard_attribute(attribute)
associated_dashboard.attribute_types[attribute.to_sym] if attribute
end

def sort_by
options[:sort_by]
end
Expand Down
4 changes: 4 additions & 0 deletions lib/administrate/field/password.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ def self.searchable?
false
end

def self.sortable?
false
end

def truncate
data.to_s.gsub(/./, character)[0...truncation_length]
end
Expand Down
17 changes: 9 additions & 8 deletions lib/administrate/order.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
module Administrate
class Order
def initialize(attribute = nil, direction = nil, association_attribute: nil)
def initialize(attribute = nil, direction = nil, association_attribute: nil, sorting_column: nil)
@attribute = attribute
@direction = sanitize_direction(direction)
@association_attribute = association_attribute
# @association_attribute = association_attribute
@sorting_column = sorting_column || attribute
end

def apply(relation)
return order_by_association(relation) unless
reflect_association(relation).nil?

order = relation.arel_table[attribute].public_send(direction)
order = relation.arel_table[sorting_column].public_send(direction)

return relation.reorder(order) if
column_exist?(relation, attribute)
column_exist?(relation, sorting_column)

relation
end
Expand All @@ -33,7 +34,7 @@ def order_params_for(attr)

private

attr_reader :attribute, :association_attribute
attr_reader :attribute, :association_attribute, :sorting_column

def sanitize_direction(direction)
%w[asc desc].include?(direction.to_s) ? direction.to_sym : :asc
Expand Down Expand Up @@ -94,9 +95,9 @@ def order_by_id(relation)
end

def ordering_by_association_column?(relation)
association_attribute &&
(attribute != sorting_column) &&
column_exist?(
reflect_association(relation).klass, association_attribute.to_sym
reflect_association(relation).klass, sorting_column.to_sym
)
end

Expand All @@ -113,7 +114,7 @@ def order_by_association_id(relation)
end

def order_by_association_attribute(relation)
order_by_association_column(relation, association_attribute)
order_by_association_column(relation, sorting_column)
end

def order_by_association_column(relation, column_name)
Expand Down
2 changes: 1 addition & 1 deletion spec/dashboards/customer_dashboard_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
expect(fields[:name]).to eq(Field::String)
expect(fields[:email]).to eq(Field::Email)
expect(fields[:lifetime_value])
.to eq(Field::Number.with_options(prefix: "$", decimals: 2))
.to eq(Field::Number.with_options(prefix: "$", decimals: 2, sortable: false))
end
end

Expand Down
2 changes: 1 addition & 1 deletion spec/example_app/app/dashboards/customer_dashboard.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class CustomerDashboard < Administrate::BaseDashboard
created_at: Field::DateTime,
email: Field::Email,
email_subscriber: Field::Boolean,
lifetime_value: Field::Number.with_options(prefix: "$", decimals: 2),
lifetime_value: Field::Number.with_options(prefix: "$", decimals: 2, sortable: false),
name: Field::String,
orders: Field::HasMany.with_options(limit: 2, sort_by: :id),
log_entries: Field::HasManyVariant.with_options(limit: 2, sort_by: :id),
Expand Down
2 changes: 1 addition & 1 deletion spec/example_app/app/dashboards/line_item_dashboard.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class LineItemDashboard < Administrate::BaseDashboard
created_at: Field::DateTime,
updated_at: Field::DateTime,
order: Field::BelongsTo,
product: Field::BelongsTo,
product: Field::BelongsTo.with_options(sorting_column: :name),
quantity: Field::Number,
total_price: Field::Number.with_options(prefix: "$", decimals: 2),
unit_price: Field::Number.with_options(prefix: "$", decimals: 2)
Expand Down
Loading

0 comments on commit 1a92f62

Please sign in to comment.