Skip to content

Commit

Permalink
Merge pull request #2972 from kulturbande/resource-table-component
Browse files Browse the repository at this point in the history
Resource Table Component
  • Loading branch information
tvdeyen authored Aug 29, 2024
2 parents 3513fd1 + 026ed13 commit 5415233
Show file tree
Hide file tree
Showing 25 changed files with 852 additions and 434 deletions.
2 changes: 1 addition & 1 deletion app/assets/builds/alchemy/admin.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion app/assets/builds/alchemy/admin.css.map

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions app/assets/stylesheets/alchemy/admin/tables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,10 @@ td.count {
padding-right: var(--spacing-4);
}

td.taggings_types {
width: 15%;
}

.list .login_status {
width: 16px;
}
Expand Down
46 changes: 46 additions & 0 deletions app/components/alchemy/admin/resource/action.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

module Alchemy
module Admin
module Resource
# Renders a container for a button, which evaluate CanCanCan and shows a tooltip. This
# is an internal component for the resource table, to make easier to read.
#
# @param [String, Symbol, nil] :name
# name of an action to evaluate if the user can perform these action on the given object
# @param [String, nil] :tooltip
# show a tooltip around the button
# @param [Lambda] :block
# a block to include a button or a link
#
class Action < ViewComponent::Base
delegate :can?, to: :helpers

attr_reader :block, :name, :tooltip

erb_template <<~ERB
<% if name.nil? || can?(name, @resource) %>
<% if tooltip.present? %>
<sl-tooltip content="<%= tooltip %>">
<%= view_context.capture(@resource, &block) %>
</sl-tooltip>
<% else %>
<%= view_context.capture(@resource, &block) %>
<% end %>
<% end %>
ERB

def initialize(name = nil, tooltip = nil, &block)
@name = name
@tooltip = tooltip
@block = block
end

def with_resource(resource)
@resource = resource
self
end
end
end
end
end
34 changes: 34 additions & 0 deletions app/components/alchemy/admin/resource/cell.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

module Alchemy
module Admin
module Resource
# Renders a table cell with the given css classes
#
# @param [String, nil] :css_classes
# css classes that are show at the table cell
# @param [Lambda] :block
# a block to include a button or a link
#
class Cell < ViewComponent::Base
attr_reader :block, :css_classes

erb_template <<~ERB
<td class="<%= css_classes %>">
<%= view_context.capture(@resource, &block) %>
</td>
ERB

def initialize(css_classes, &block)
@css_classes = css_classes
@block = block
end

def with_resource(resource)
@resource = resource
self
end
end
end
end
end
46 changes: 46 additions & 0 deletions app/components/alchemy/admin/resource/header.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

module Alchemy
module Admin
module Resource
# Renders a table header tag
# the component is an internal component of the Table component
#
# @param [String] :name
# name of the sortable link or the text if not additional text is given
# @param [String] :query
# Ransack query
# @param [String] :css_classes ("")
# css class of the th - tag
# @param [String, nil] :text (nil)
# optional text of the header
# @param [Symbol] :type (:string)
# type of the column will be used to inverse the sorting order for data/time - objects
# @param [Boolean] :sortable (false)
# enable a sortable link
#
class Header < ViewComponent::Base
delegate :sort_link, to: :helpers

erb_template <<~ERB
<th class="<%= @css_classes %>">
<% if @sortable %>
<%= sort_link @query, @name, @text, default_order: @default_order %>
<% else %>
<%= @text %>
<% end %>
</th>
ERB

def initialize(name, query, css_classes: "", text: nil, type: :string, sortable: false)
@name = name
@query = query
@text = text || name
@css_classes = css_classes
@default_order = /date|time/.match?(type.to_s) ? "desc" : "asc"
@sortable = sortable
end
end
end
end
end
150 changes: 150 additions & 0 deletions app/components/alchemy/admin/resource/table.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# frozen_string_literal: true

module Alchemy
module Admin
module Resource
# Renders a resource table with columns and buttons
#
# == Example
#
# <%= render Alchemy::Admin::Resource::Table.new(@languages, query: @query) do |table| %>
# <% table.icon_column "translate-2", style: false %>
# <% table.column :name, sortable: true %>
# <% table.column :language_code, sortable: true %>
# <% table.column :page_layout do |language| %>
# <%= Alchemy::Page.human_layout_name(language.page_layout) %>
# <% end %>
# <% table.delete_button %>
# <% table.edit_button %>
# <% end %>
#
# @param [ActiveRecord::Relation] :collection
# a collection of Alchemy::Resource objects that are shown in the table
# @param [Ransack::Search] :query
# The ransack search object to allow sortable table columns
# @param [String] :nothing_found_label (Alchemy.t("Nothing found"))
# The message that will be shown, if the collection is empty
# @param [Hash] :search_filter_params ({})
# An additional hash that will attached to the delete and edit button to redirect back to
# the same page of the table
# @param [String] :icon (nil)
# a default icon, if the table is auto generated
class Table < ViewComponent::Base
delegate :render_attribute,
:resource_path,
:render_icon,
:edit_resource_path,
:resource_handler,
:resource_window_size,
to: :helpers

attr_reader :collection,
:nothing_found_label,
:search_filter_params

renders_many :headers, Header

renders_many :cells, ->(css_classes, &block) do
Cell.new(css_classes, &block)
end

renders_many :actions, ->(name, tooltip = nil, &block) do
Action.new(name, tooltip, &block)
end

erb_template <<~ERB
<% if collection.any? %>
<table class="list">
<thead>
<tr>
<% headers.each do |header| %>
<%= header %>
<% end %>
<% if actions? %>
<th class="tools"></th>
<% end %>
</tr>
</thead>
<tbody>
<% collection.each do |resource| %>
<tr class="<%= cycle('even', 'odd') %>">
<% cells.each do |cell| %>
<%= render cell.with_resource(resource) %>
<% end %>
<% if actions? %>
<td class="tools">
<% actions.each do |action| %>
<%= render action.with_resource(resource) %>
<% end %>
</td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<alchemy-message type="info">
<%= nothing_found_label %>
</alchemy-message>
<% end %>
ERB

def initialize(collection, query: nil, nothing_found_label: Alchemy.t("Nothing found"), search_filter_params: {}, icon: nil)
@collection = collection
@query = query
@nothing_found_label = nothing_found_label
@search_filter_params = search_filter_params
@icon = icon
end

def column(name, header: nil, sortable: false, type: nil, class_name: nil, &block)
header ||= resource_handler.model.human_attribute_name(name)
type ||= resource_handler.model.columns_hash[name.to_s]&.type
attribute = resource_handler.attributes.find { |item| item[:name] == name.to_s } || {name: name, type: type}
block ||= lambda { |item| render_attribute(item, attribute) }

css_classes = [name, type, class_name].compact.join(" ")
with_header(name, @query, css_classes: css_classes, text: header, type: type, sortable: sortable)
with_cell(css_classes, &block)
end

def icon_column(icon = nil, style: nil)
column(:icon, header: "") do |resource|
render_icon(icon || yield(resource), size: "xl", style: style)
end
end

def delete_button(tooltip: Alchemy.t("Delete"), message: Alchemy.t("Are you sure?"))
with_action(:destroy, tooltip) do |row|
helpers.delete_button(resource_path(row, search_filter_params), {message: message})
end
end

def edit_button(tooltip: Alchemy.t("Edit"), size: resource_window_size)
with_action(:edit, tooltip) do |row|
helpers.link_to_dialog render_icon(:edit),
edit_resource_path(row, search_filter_params),
{size: size},
class: "icon_button"
end
end

private

##
# if no cells are available the resource_helper will be used, to generate the
# default attributes of the given resource
def before_render
unless cells?
icon_column(@icon) if @icon.present?
resource_handler.sorted_attributes.each do |attribute|
column(attribute[:name], sortable: true)
end
delete_button
edit_button
end
end
end
end
end
end
2 changes: 1 addition & 1 deletion app/controllers/alchemy/admin/resources_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ def is_alchemy_module?
end

def alchemy_module
@alchemy_module ||= module_definition_for(controller: params[:controller], action: "index")
@alchemy_module ||= module_definition_for(controller: controller_path, action: "index")
end

def load_resource
Expand Down
81 changes: 0 additions & 81 deletions app/views/alchemy/admin/attachments/_attachment.html.erb

This file was deleted.

Loading

0 comments on commit 5415233

Please sign in to comment.