Skip to content

Commit

Permalink
Add koi specific custom typed cells
Browse files Browse the repository at this point in the history
  • Loading branch information
AlanCornthwaiteKatalyst committed May 13, 2024
1 parent 49190fb commit aecb45b
Show file tree
Hide file tree
Showing 8 changed files with 348 additions and 7 deletions.
60 changes: 60 additions & 0 deletions app/components/koi/tables/cells/attachment_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

module Koi
module Tables
module Cells
# Shows an attachment
#
# The value is expected to be an ActiveStorage attachment
#
# If it is representable, shows as a image tag using the specified variant.
#
# Otherwise shows as a link to download.
class AttachmentComponent < Katalyst::Tables::CellComponent
def initialize(variant:, **)
super(**)

@variant = variant
end

def rendered_value
representation
end

def representation
if value.try(:variable?) && named_variant.present?
image_tag(value.variant(@variant))
elsif value.try(:attached?)
filename.to_s
else
""
end
end

def filename
value.blob.filename
end

# Utility for accessing the path Rails provides for retrieving the
# attachment for use in cells. Example:
# <% row.attachment :file do |cell| %>
# <%= link_to "Download", cell.internal_path %>
# <% end %>
def internal_path
rails_blob_path(value, disposition: :attachment)
end

private

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

# Find the reflective variant by name (i.e. :thumb by default)
def named_variant
record.attachment_reflections[@column.to_s].named_variants[@variant.to_sym]
end
end
end
end
end
43 changes: 43 additions & 0 deletions app/components/koi/tables/cells/link_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

module Koi
module Tables
module Cells
# Displays a link to the record
# The link text is the value of the attribute
class LinkComponent < Katalyst::Tables::CellComponent
define_html_attribute_methods :link_attributes

def initialize(url:, link:, **)
super(**)

@url = url

self.link_attributes = link
end

def rendered_value
link_to(value, url, **link_attributes)
end

def url
case @url
when Symbol
# helpers are not available until the component is rendered
@url = helpers.public_send(@url, record)
when Proc
@url = @url.call(record)
else
@url
end
end

private

def default_html_attributes
{ class: "type-link" }
end
end
end
end
end
52 changes: 50 additions & 2 deletions app/components/koi/tables/table_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,57 @@

module Koi
module Tables
# Custom table component, in order to override the default header and body row components
# which enables us to use our own custom header and body cell components
class TableComponent < Katalyst::TableComponent
# Generates a column that links to the record's show page (by default).
#
# @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 url [Symbol|String|Proc] arguments for url_For, defaults to the record
# @param link [Hash] options to be passed to the link_to helper
# @param ** [Hash] HTML attributes to be added to column cells
# @param & [Proc] optional block to alter the cell content
#
# If a block is provided, it will be called with the link cell component as an argument.
# @yieldparam cell [Katalyst::Tables::Cells::LinkComponent] the cell component
#
# @return [void]
#
# @example Render a column containing the record's title, linked to its show page
# <% row.link :title %> # => <td><a href="/admin/post/15">About us</a></td>
# @example Render a column containing the record's title, linked to its edit page
# <% row.link :title, url: :edit_admin_post_path do |cell| %>
# Edit <%= cell %>
# <% end %>
# # => <td><a href="/admin/post/15/edit">Edit About us</a></td>
def link(column, label: nil, heading: false, url: [:admin, record], link: {}, **, &)
with_cell(Tables::Cells::LinkComponent.new(
collection:, row:, column:, record:, label:, heading:, url:, link:, **,
), &)
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
# @param label [String|nil] the label to use for the column header
# @param heading [boolean] if true, data cells will use `th` tags
# @param variant [Symbol] the variant to use when rendering the image (default :thumb)
# @param ** [Hash] HTML attributes to be added to column cells
# @param & [Proc] optional block to alter the cell content
#
# If a block is provided, it will be called with the attachment cell component as an argument.
# @yieldparam cell [Katalyst::Tables::Cells::AttachmentComponent] the cell component
#
# @return [void]
#
# @example Render a column containing a download link to the record's background image
# <% row.attachment :background %> # => <td><a href="...">background.png</a></td>
def attachment(column, label: nil, heading: false, variant: :thumb, **, &)
with_cell(Tables::Cells::AttachmentComponent.new(
collection:, row:, column:, record:, label:, heading:, variant:, **,
), &)
end

def default_html_attributes
{ class: "index-table" }
end
Expand Down
83 changes: 83 additions & 0 deletions spec/components/koi/tables/cells/attachment_component_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe Koi::Tables::Cells::AttachmentComponent do
let(:table) { Koi::Tables::TableComponent.new(collection:) }
let(:collection) { create_list(:banner, 1, :with_image) }
let(:rendered) { render_inline(table) { |row| row.attachment(:image) } }
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-attachment">Image</th>
HTML
end

it "renders column data" do
expect(data).to have_css("td.type-attachment img[src*='dummy.png']")
end

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

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

it "renders data with html_options" do
expect(data).to have_css("td.type-attachment.CLASS[data-foo=bar] img[src*='dummy.png']")
end
end

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

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

it "renders data without label" do
expect(data).to have_css("td.type-attachment img[src*='dummy.png']")
end
end

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

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

context "with nil data value" do
let(:collection) { build_list(:banner, 1) }

it "renders data as falsey" do
expect(data).to match_html(<<~HTML)
<td class="type-attachment"></td>
HTML
end
end

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

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

it "renders the custom data" do
expect(data).to have_css("td.type-attachment > span > img[src*='dummy.png']")
end
end
end
92 changes: 92 additions & 0 deletions spec/components/koi/tables/cells/link_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::LinkComponent do
let(:table) { Koi::Tables::TableComponent.new(collection:) }
let(:collection) { create_list(:post, 1) }
let(:rendered) { render_inline(table) { |row, _post| row.link(:name) } }
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-link">Name</th>
HTML
end

it "renders column data" do
expect(data).to match_html(<<~HTML)
<td class="type-link"><a href="/admin/posts/#{collection.first.id}">#{collection.first.name}</a></td>
HTML
end

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

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

it "renders data with html_options" do
expect(data).to match_html(<<~HTML)
<td id="ID" class="type-link CLASS" style="style" data-foo="bar" aria-label="LABEL"><a href="/admin/posts/#{collection.first.id}">#{collection.first.name}</a></td>
HTML
end
end

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

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

it "renders data without label" do
expect(data).to match_html(<<~HTML)
<td class="type-link"><a href="/admin/posts/#{collection.first.id}">#{collection.first.name}</a></td>
HTML
end
end

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

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

context "with nil data value" do
let(:rendered) { render_inline(table) { |row| row.link(:name) } }
let(:collection) { create_list(:post, 1, name: nil) }

it "renders data as url" do
expect(data).to match_html(<<~HTML)
<td class="type-link"><a href="/admin/posts/#{collection.first.id}">/admin/posts/#{collection.first.id}</a></td>
HTML
end
end

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

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

it "renders the custom data" do
expect(data).to match_html(<<~HTML)
<td class="type-link"><span><a href="/admin/posts/#{collection.first.id}">#{collection.first.name}</a></span></td>
HTML
end
end
end
14 changes: 14 additions & 0 deletions spec/support/frontend_examples.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

require "rack/request"
require "rails_helper"

module Test
HTML_ATTRIBUTES = {
id: "ID",
class: "CLASS",
html: { style: "style" },
data: { foo: "bar" },
aria: { label: "LABEL" },
}.freeze
end
6 changes: 3 additions & 3 deletions spec/support/match_html.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ def initialize(expected_html, **options)
def matches?(response)
case response
when Nokogiri::XML::Node
@actual_doc = response
@actual_html = response.to_html
@actual_doc = Nokogiri::HTML.fragment(@actual_html)
else
@actual_html = response
@actual_doc = Nokogiri::HTML.fragment(response)
Expand Down Expand Up @@ -57,8 +57,8 @@ def diff

module RSpec
module Matchers
def match_html(expected_html, **options)
HTMLMatcher.new(expected_html, **options)
def match_html(expected_html, **)
HTMLMatcher.new(expected_html, **)
end
end
end
5 changes: 3 additions & 2 deletions spec/views/admin/posts/index.html.erb_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@
view.extend(Pagy::Frontend)

collection = Admin::PostsController::Collection.new.apply(Post.all)
table = Koi::Tables::TableComponent.new(collection:, id: "index-table")

# Workaround for https://github.com/rspec/rspec-rails/issues/2729
view.lookup_context.prefixes.prepend "admin/posts"

render locals: { collection:, table: }
allow(view).to receive(:default_table_component_class).and_return(Koi::Tables::TableComponent)

render locals: { collection: }
end

it { expect(rendered).to have_css("th", text: "Name") }
Expand Down

0 comments on commit aecb45b

Please sign in to comment.