Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resource Table Component #2972

Merged
merged 12 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
sascha-karnatz marked this conversation as resolved.
Show resolved Hide resolved
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