Clickable rows and slim, compact design.
Filters
Drag to reorder, pin columns, and show/hide:
Table definition
app/infotable/comments_table.rb
# ============================================================================
# COMMENTS TABLE - Demonstrates VIRTUAL COLUMNS with filtering
# ============================================================================
#
# This table showcases how to use VIRTUAL (computed) columns for filtering
# and sorting on aggregated data.
#
# VIRTUAL COLUMN EXAMPLE:
# - Column: author_total_comments
# - Type: Subquery (SELECT COUNT(*) ...)
# - Features: Sortable ✅ and Filterable ✅
# - Implementation: Mark as `virtual: true` to enable HAVING clause filtering
#
# See VIRTUAL_COLUMNS_EXAMPLE.md for more examples
# ============================================================================
class CommentsTable < Infotable::Base
query do
Comment.select(
"comments.*",
"(SELECT COUNT(*) FROM comments AS c WHERE c.author_id = comments.author_id) AS author_total_comments"
).includes(:post, :author)
end
column :id,
label: "ID",
type: :integer,
width: "60px",
sortable: true,
visible: false
column :post,
label: "Post",
type: :string,
width: "300px",
sortable: true,
searchable: true,
filterable: true,
filter_type: :select,
filter_options: -> { Post.order(:title).limit(100).pluck(:title, :id).map { |title, id| [title.to_s.truncate(50), id] } },
visible: true,
formatter: ->(value, record) {
post_title = record.post.title.to_s.truncate(45)
"<span class='text-sm text-gray-700'>#{post_title}</span>".html_safe
}
column :author,
label: "Commenter",
type: :string,
width: "180px",
sortable: true,
searchable: true,
filterable: true,
filter_type: :select,
filter_options: -> { Author.order(:name).pluck(:name, :id).map { |name, id| [name, id] } },
visible: true,
formatter: ->(value, record) {
return "-" unless record.author
author_name = record.author.name
if record.author.verified
"#{author_name} <span class='text-xs text-green-600'>✓</span>".html_safe
else
author_name
end
}
column :body,
label: "Comment",
type: :text,
searchable: true,
visible: true,
formatter: ->(value, record) {
truncated = value.to_s.truncate(80)
"<span class='text-sm'>#{truncated}</span>".html_safe
}
column :status,
label: "Status",
type: :badge,
sortable: true,
filterable: true,
filter_type: :select,
filter_options: ["approved", "pending", "spam"],
visible: true,
formatter: {
type: :badge,
options: {
colors: {
"approved" => "green",
"pending" => "yellow",
"spam" => "red"
}
}
}
column :flagged,
label: "Flagged",
type: :boolean,
sortable: true,
filterable: true,
filter_type: :select,
filter_options: [["Yes", true], ["No", false]],
visible: true,
formatter: ->(value, record) {
if value
'<span class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-red-100 text-red-800">🚩 Flagged</span>'.html_safe
else
'<span class="text-gray-400 text-xs">-</span>'.html_safe
end
}
column :likes_count,
label: "Likes",
type: :integer,
sortable: true,
visible: true,
formatter: ->(value, record) {
count = value || 0
if count > 0
"👍 #{count}"
else
"-"
end
}
column :author_total_comments,
label: "Author's Total",
type: :integer,
width: "140px",
sortable: true,
filterable: true,
filter_type: :number,
virtual: true,
virtual_sql: "(SELECT COUNT(*) FROM comments AS c WHERE c.author_id = comments.author_id)",
visible: true,
formatter: ->(value, record) {
count = value || 0
color_class = if count > 50
"bg-purple-100 text-purple-800 border-purple-200"
elsif count > 20
"bg-blue-100 text-blue-800 border-blue-200"
elsif count > 5
"bg-green-100 text-green-800 border-green-200"
else
"bg-gray-100 text-gray-600 border-gray-200"
end
"<span class='inline-flex items-center px-2 py-0.5 rounded text-xs font-medium border #{color_class}'>#{count} comments</span>".html_safe
}
column :created_at,
label: "Posted",
type: :datetime,
sortable: true,
filterable: true,
filter_type: :date,
visible: true,
formatter: ->(value, record) {
time_ago = ((Time.now - value) / 3600).round
if time_ago < 1
"Just now"
elsif time_ago < 24
"#{time_ago}h ago"
elsif time_ago < 168
"#{(time_ago / 24).round}d ago"
else
value.strftime("%b %d, %Y")
end
}
column :actions,
label: "Actions",
type: :action,
visible: true,
formatter: :action,
formatter_options: {
actions: [
{
label: "View",
url: ->(record) { "/comments/#{record.id}" },
color: "primary"
},
{
label: "Edit",
url: ->(record) { "/comments/#{record.id}/edit" }
},
{
label: "Delete",
url: ->(record) { "/comments/#{record.id}" },
method: :delete,
color: "danger",
confirm: "Are you sure?"
}
]
}
per_page 30
per_page_options [10, 30, 50, 100]
default_sort :created_at, :desc
table_min_height "450px"
table_max_height "650px"
row_link ->(record) { "/comments/#{record.id}" }
end