Skip to content

Commit

Permalink
Enum component for tables
Browse files Browse the repository at this point in the history
  • Loading branch information
sfnelson committed Jun 12, 2024
1 parent 269cca0 commit 16a27f6
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 7 deletions.
14 changes: 14 additions & 0 deletions app/assets/stylesheets/koi/base/_tables.scss
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
@use "katalyst/tables";

:where(th.type-enum, td.type-enum) {
width: var(--width-small);
}

:where(td.type-enum span) {
--background-color: var(--site-primary-light);
--color: var(--site-on-primary);
background: var(--background-color);
color: var(--color);
border-radius: 0.25rem;
font-size: var(--paragraph--small);
padding: 0.25rem 0.5rem;
}
27 changes: 27 additions & 0 deletions app/components/koi/tables/cells/enum_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module Koi
module Tables
module Cells
# Displays an enum value using data inferred from the model.
class EnumComponent < Katalyst::Tables::CellComponent
def rendered_value
if (value = self.value).present?
label = t(i18n_enum_label_key(value), default: value)
tag.span(label, data: { enum: column, value: })
end
end

private

def default_html_attributes
{ class: "type-enum" }
end

def i18n_enum_label_key(value)
"active_record.attributes.#{collection.model_name.i18n_key}/#{column}.#{value}"
end
end
end
end
end
28 changes: 28 additions & 0 deletions app/components/koi/tables/table_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,34 @@ def link(column, label: nil, heading: false, url: [:admin, record], link: {}, **
), &)
end

# Generates a column from an enum value rendered as a tag.
# The target attribute must be defined as an `enum` in the model.
#
# @param column [Symbol] the column's name, called as a method on the record.
# @param label [String|nil] the label to use for the column header
# @param heading [boolean] if true, data cells will use `th` tags
# @param ** [Hash] HTML attributes to be added to column cells
# @param & [Proc] optional block to wrap the cell content
#
# When rendering an enum value, the component will check for translations
# using the key `active_record.attributes.[model]/[column].[value]`,
# e.g. `active_record.attributes.banner/status.published`.
#
# If a block is provided, it will be called with the cell component as an argument.
# @yieldparam cell [Katalyst::Tables::CellComponent] the cell component
#
# @return [void]
#
# @example Render a generic text column for any value that supports `to_s`
# <% row.enum :status %>
# <%# label => <th>Status</th> %>
# <%# data => <td class="type-enum"><span data-enum="status" data-value="published">Published</span></td> %>
def enum(column, label: nil, heading: false, **, &)
with_cell(Tables::Cells::EnumComponent.new(
collection:, row:, column:, record:, label:, heading:, **,
), &)
end

# Generates a column that renders an ActiveStorage attachment as a downloadable link.
#
# @param column [Symbol] the column's name, called as a method on the record
Expand Down
8 changes: 1 addition & 7 deletions lib/tasks/dummy.thor
Original file line number Diff line number Diff line change
Expand Up @@ -93,18 +93,12 @@ class Dummy < Thor
inside("spec/dummy") do
run <<~SH
rails g koi:admin Post name:string title:string content:rich_text active:boolean published_on:date
rails g koi:admin Banner name:string image:attachment ordinal:integer
rails g koi:admin Banner name:string image:attachment ordinal:integer status:integer
SH

run "rails db:migrate"
end

gsub_file("app/models/banner.rb", "has_one_attached :image\n", <<~RUBY)
has_one_attached :image do |image|
image.variant :thumb, resize_to_fill: [100, 100]
end
RUBY

gsub_file("config/routes/admin.rb", "resources :banners\n", <<~RUBY)
resources :banners do
patch :order, on: :collection
Expand Down
92 changes: 92 additions & 0 deletions spec/components/koi/tables/cells/enum_component_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe Koi::Tables::Cells::EnumComponent do
let(:table) { Koi::Tables::TableComponent.new(collection:) }
let(:collection) { create_list(:banner, 1, status: "published") }
let(:rendered) { render_inline(table) { |row, _post| row.enum(:status) } }
let(:label) { rendered.at_css("thead th") }
let(:data) { rendered.at_css("tbody td") }

it "renders column header" do
expect(label).to match_html(<<~HTML)
<th class="type-enum">Status</th>
HTML
end

it "renders column data" do
expect(data).to match_html(<<~HTML)
<td class="type-enum"><span data-enum="status" data-value="published">Published</span></td>
HTML
end

context "with html_options" do
let(:rendered) { render_inline(table) { |row| row.enum(:status, **Test::HTML_ATTRIBUTES) } }

it "renders header with html_options" do
expect(label).to match_html(<<~HTML)
<th id="ID" class="type-enum CLASS" style="style" data-foo="bar" aria-label="LABEL">Status</th>
HTML
end

it "renders data with html_options" do
expect(data).to match_html(<<~HTML)
<td id="ID" class="type-enum CLASS" style="style" data-foo="bar" aria-label="LABEL"><span data-enum="status" data-value="published">Published</span></td>
HTML
end
end

context "when given a label" do
let(:rendered) { render_inline(table) { |row| row.enum(:status, label: "LABEL") } }

it "renders header with label" do
expect(label).to match_html(<<~HTML)
<th class="type-enum">LABEL</th>
HTML
end

it "renders data without label" do
expect(data).to match_html(<<~HTML)
<td class="type-enum"><span data-enum="status" data-value="published">Published</span></td>
HTML
end
end

context "when given an empty label" do
let(:rendered) { render_inline(table) { |row| row.enum(:status, label: "") } }

it "renders header with an empty label" do
expect(label).to match_html(<<~HTML)
<th class="type-enum"></th>
HTML
end
end

context "with nil data value" do
let(:rendered) { render_inline(table) { |row| row.enum(:status) } }
let(:collection) { create_list(:banner, 1, status: nil) }

it "renders an empty cell" do
expect(data).to match_html(<<~HTML)
<td class="type-enum"></td>
HTML
end
end

context "when given a block" do
let(:rendered) { render_inline(table) { |row| row.enum(:status) { |cell| cell.tag.span(cell) } } }

it "renders the default header" do
expect(label).to match_html(<<~HTML)
<th class="type-enum">Status</th>
HTML
end

it "renders the custom data" do
expect(data).to match_html(<<~HTML)
<td class="type-enum"><span><span data-enum="status" data-value="published">Published</span></span></td>
HTML
end
end
end
15 changes: 15 additions & 0 deletions spec/templates/app/models/banner.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

class Banner < ApplicationRecord
enum :status, { draft: 0, published: 1, archived: 2 }, default: :draft

has_one_attached :image do |image|
image.variant :thumb, resize_to_fill: [100, 100]
end

scope :admin_search, ->(query) do
where("name LIKE :query", query: "%#{query}%")
end

default_scope -> { order(ordinal: :asc) }
end
7 changes: 7 additions & 0 deletions spec/templates/config/locales/en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
en:
active_record:
attributes:
banner/status:
draft: Draft
published: Published
archived: Archived
1 change: 1 addition & 0 deletions spec/templates/spec/factories/banners.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
factory :banner do
name { Faker::Kpop.solo }
sequence(:ordinal)
status { %i[draft published archived].sample }

trait :with_image do
image { Rack::Test::UploadedFile.new(Rails.root.join("../fixtures/images/dummy.png"), "image/png") }
Expand Down

0 comments on commit 16a27f6

Please sign in to comment.