Skip to content

Commit

Permalink
Add sorting_field to Field
Browse files Browse the repository at this point in the history
  • Loading branch information
goosys committed Sep 13, 2024
1 parent f377679 commit c4f4703
Show file tree
Hide file tree
Showing 12 changed files with 163 additions and 28 deletions.
9 changes: 9 additions & 0 deletions app/controllers/administrate/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ def order
sorting_direction,
association_attribute: order_by_field(
dashboard_attribute(sorting_attribute)
),
sorting_field: sorting_field(
dashboard_attribute(sorting_params.fetch(:order, nil))
)
)
end
Expand All @@ -153,6 +156,12 @@ def order_by_field(dashboard)
dashboard.options.fetch(:order, nil)
end

def sorting_field(dashboard)
return unless dashboard.try(:options)

dashboard.options.fetch(:sorting_field, nil)
end

def dashboard_attribute(attribute)
dashboard.attribute_types[attribute.to_sym] if attribute
end
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
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
8 changes: 8 additions & 0 deletions lib/administrate/field/deferred.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ def searchable_fields
end
end

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

def sortable_field
options.fetch(:sortable_field, nil)
end

def permitted_attribute(attr, opts = {})
if options.key?(:foreign_key)
Administrate.warn_of_deprecated_option(:foreign_key)
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
15 changes: 8 additions & 7 deletions lib/administrate/order.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
module Administrate
class Order
def initialize(attribute = nil, direction = nil, association_attribute: nil)
def initialize(attribute = nil, direction = nil, association_attribute: nil, sorting_field: nil)
@attribute = attribute
@sorting_field = sorting_field.presence || attribute
@direction = sanitize_direction(direction)
@association_attribute = association_attribute
end
Expand All @@ -10,10 +11,10 @@ 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_field].public_send(direction)

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

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

private

attr_reader :attribute, :association_attribute
attr_reader :attribute, :sorting_field, :association_attribute

def sanitize_direction(direction)
%w[asc desc].include?(direction.to_s) ? direction.to_sym : :asc
Expand Down Expand Up @@ -68,7 +69,7 @@ def order_by_count(relation)
klass = reflect_association(relation).klass
query = klass.arel_table[klass.primary_key].count.public_send(direction)
relation
.left_joins(attribute.to_sym)
.left_joins(sorting_field.to_sym)
.group(:id)
.reorder(query)
end
Expand Down Expand Up @@ -118,7 +119,7 @@ def order_by_association_attribute(relation)

def order_by_association_column(relation, column_name)
relation.joins(
attribute.to_sym
sorting_field.to_sym
).reorder(order_by_association_column_query(relation, column_name))
end

Expand All @@ -132,7 +133,7 @@ def relation_type(relation)
end

def reflect_association(relation)
relation.klass.reflect_on_association(attribute.to_s)
relation.klass.reflect_on_association(sorting_field.to_s)
end

def association_foreign_key(relation)
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
3 changes: 2 additions & 1 deletion spec/example_app/app/dashboards/customer_dashboard.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ 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,
nickname: Field::String.with_options(
getter: ->(field) { "Mr. #{field.resource.name}man" if field.resource.name.present? },
sorting_field: :name,
searchable: false
),
orders: Field::HasMany.with_options(limit: 2, sort_by: :id),
Expand Down
6 changes: 5 additions & 1 deletion spec/example_app/app/dashboards/order_dashboard.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ class OrderDashboard < Administrate::BaseDashboard
address_city: Field::String,
address_state: Field::String,
address_zip: Field::String,
address: Field::String.with_options(
sorting_field: :address_zip,
searchable: false
),
customer: Field::BelongsTo.with_options(order: "name"),
line_items: Field::HasMany.with_options(
collection_attributes: %i[product quantity unit_price total_price]
Expand All @@ -29,7 +33,7 @@ class OrderDashboard < Administrate::BaseDashboard
COLLECTION_ATTRIBUTES = [
:id,
:customer,
:address_state,
:address,
:total_price,
:line_items,
:shipped_at
Expand Down
4 changes: 4 additions & 0 deletions spec/example_app/app/models/order.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ class Order < ApplicationRecord
def total_price
line_items.map(&:total_price).reduce(0, :+)
end

def address
[address_line_two, address_line_one, address_city, address_state, address_zip].compact.join(" ")
end
end
58 changes: 58 additions & 0 deletions spec/features/sort_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
require "rails_helper"

feature "Sort" do
describe "sortable" do
context "when the field is sortable" do
it "is a link to sort by that field" do
visit admin_customers_path
expect(page).to have_link("Name")
end
end

context "when the field is NOT sortable" do
it "is not a link" do
visit admin_customers_path
expect(page).not_to have_link("Password")
end
end
end

scenario "admin sorts customers by name" do
dan = create(:customer, name: "Dan Croak")
bob = create(:customer, name: "Bob")
alice = create(:customer, name: "Alice")

visit admin_customers_path
click_on "Name"

expect(page).to have_css("table tr:nth-child(1)", text: alice.name)
expect(page).to have_css("table tr:nth-child(2)", text: bob.name)
expect(page).to have_css("table tr:nth-child(3)", text: dan.name)
end

scenario "admin sorts customers by nickname (virtual field)" do
dan = create(:customer, name: "Dan Croak")
bob = create(:customer, name: "Bob")
alice = create(:customer, name: "Alice")

visit admin_customers_path
click_on "Nickname"

expect(page).to have_css("table tr:nth-child(1)", text: alice.name)
expect(page).to have_css("table tr:nth-child(2)", text: bob.name)
expect(page).to have_css("table tr:nth-child(3)", text: dan.name)
end

scenario "admin sorts orders by address (virtual field)" do
create(:order, address_zip: "651", address_state: "UT")
create(:order, address_zip: "7", address_state: "WV")
create(:order, address_zip: "59543-0366", address_state: "NJ")

visit admin_orders_path
click_on "Address"

expect(page).to have_css("table tr:nth-child(1)", text: "NJ 59543-0366")
expect(page).to have_css("table tr:nth-child(2)", text: "UT 651")
expect(page).to have_css("table tr:nth-child(3)", text: "WV 7")
end
end
32 changes: 32 additions & 0 deletions spec/lib/fields/deferred_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,38 @@
end
end

describe "#sortable?" do
context "when given a `sortable` option" do
it "returns the value given" do
sortable_deferred = Administrate::Field::Deferred.new(
double(sortable?: false),
sortable: true
)
unsortable_deferred = Administrate::Field::Deferred.new(
double(sortable?: true),
sortable: false
)

expect(sortable_deferred.sortable?).to eq(true)
expect(unsortable_deferred.sortable?).to eq(false)
end
end

context "when not given a `sortable` option" do
it "falls back to the default of the deferred class" do
sortable_deferred = Administrate::Field::Deferred.new(
double(sortable?: true)
)
unsortable_deferred = Administrate::Field::Deferred.new(
double(sortable?: false)
)

expect(sortable_deferred.sortable?).to eq(true)
expect(unsortable_deferred.sortable?).to eq(false)
end
end
end

describe "#==" do
it "returns false for different deferred classes" do
one = Administrate::Field::Deferred.new(String)
Expand Down

0 comments on commit c4f4703

Please sign in to comment.