Skip to content
Draft
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
55 changes: 17 additions & 38 deletions app/components/shared/index_table_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<div data-controller="index-table-component" data-index-table-component-per-page-value="<%= per_page %>">
<div
data-controller="index-table-component"
data-action="resize@window->index-table-component#refreshLayout"
data-index-table-component-per-page-value="<%= per_page %>">
<% if searchable %>
<div class="mb-4">
<label class="input input-bordered w-full">
Expand All @@ -14,42 +17,18 @@
</div>
<% end %>

<!-- Construct columns sizes string from individual col_size parameters -->
<% column_sizes = columns.map { |c| c.col_size || "minmax(0, 1fr)" }.join(" ") %>
<%= render Shared::IndexTableComponent::DesktopComponent.new(
records: records,
columns: columns,
column_sizes: column_sizes,
empty_state_text: empty_state_text,
cell_content_renderer: cell_content_renderer
) %>

<!-- Each row (including header) is its own rounded box -->
<div class="mb-2">
<div class="card bg-neutral rounded-lg">
<div class="p-4 grid items-center gap-4" style="grid-template-columns: <%= column_sizes %>">
<% columns.each do |column| %>
<div class="text-sm font-semibold text-neutral-content"><%= column.header %></div>
<% end %>
</div>
</div>
</div>

<div class="space-y-2">
<% records.each_with_index do |record, index| %>
<div data-index-table-component-target="row" class="card bg-white rounded-lg shadow-sm">
<div class="p-3 grid items-center gap-4" style="grid-template-columns: <%= column_sizes %>">
<% columns.each do |column| %>
<div class="text-sm text-base-content"><%= render_cell_content(record, column) %></div>
<% end %>
</div>
</div>
<% end %>
<div data-index-table-component-target="emptyRow" class="card bg-white rounded-lg shadow-sm <%= "hidden" unless empty? %>">
<div class="flex justify-center p-3 items-center">
<div class="text-sm text-base-content/40 font-bold"><%= t("shared.index_table_component.no_entries") %></div>
</div>
</div>
</div>

<div class="flex justify-center mt-4">
<div class="join hidden" data-index-table-component-target="pagination">
<button class="join-item btn btn-sm" data-action="index-table-component#previousPage" data-prev>&lt;</button>
<span class="join-item btn btn-sm no-animation pointer-events-none" data-index-table-component-target="pageInfo"></span>
<button class="join-item btn btn-sm" data-action="index-table-component#nextPage" data-next>&gt;</button>
</div>
</div>
<%= render Shared::IndexTableComponent::MobileComponent.new(
records: records,
columns: columns,
empty_state_text: empty_state_text,
cell_content_renderer: cell_content_renderer
) %>
</div>
12 changes: 12 additions & 0 deletions app/components/shared/index_table_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ def column(attribute, header: nil, col_size: nil, &cell_renderer)
@columns << Column.new(attribute, label, cell_renderer, col_size)
end

def column_sizes
@column_sizes ||= columns.map { |column| column.col_size || "minmax(0, 1fr)" }.join(" ")
end

def empty_state_text
t("shared.index_table_component.no_entries")
end

def cell_content_renderer
method(:render_cell_content)
end

private

def render_cell_content(record, column)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<div class="hidden sm:block" data-index-table-component-layout="desktop">
<div class="mb-2">
<div class="card bg-neutral rounded-lg">
<div class="p-4 grid items-center gap-4" style="grid-template-columns: <%= column_sizes %>">
<% columns.each do |column| %>
<div class="text-sm font-semibold text-neutral-content"><%= column.header %></div>
<% end %>
</div>
</div>
</div>

<div class="space-y-2">
<% records.each do |record| %>
<div data-index-table-component-target="row" class="card bg-white rounded-lg shadow-sm">
<div class="p-3 grid items-center gap-4" style="grid-template-columns: <%= column_sizes %>">
<% columns.each do |column| %>
<div class="text-sm text-base-content"><%= cell_content_renderer.call(record, column) %></div>
<% end %>
</div>
</div>
<% end %>

<div data-index-table-component-target="emptyRow" class="card bg-white rounded-lg shadow-sm hidden">
<div class="flex items-center justify-center p-3">
<div class="text-sm font-bold text-base-content/40"><%= empty_state_text %></div>
</div>
</div>
</div>

<div class="flex justify-center mt-4">
<div class="join hidden" data-index-table-component-target="pagination">
<button class="join-item btn btn-sm" data-action="index-table-component#previousPage" data-prev>&lt;</button>
<span class="join-item btn btn-sm no-animation pointer-events-none" data-index-table-component-target="pageInfo"></span>
<button class="join-item btn btn-sm" data-action="index-table-component#nextPage" data-next>&gt;</button>
</div>
</div>
</div>
16 changes: 16 additions & 0 deletions app/components/shared/index_table_component/desktop_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module Shared
class IndexTableComponent
class DesktopComponent < ViewComponent::Base
attr_reader :records, :columns, :column_sizes, :empty_state_text, :cell_content_renderer

def initialize(records:, columns:, column_sizes:, empty_state_text:, cell_content_renderer:)
super()
@records = records
@columns = columns
@column_sizes = column_sizes
@empty_state_text = empty_state_text
@cell_content_renderer = cell_content_renderer
end
end
Comment on lines +1 to +14
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<div class="sm:hidden space-y-3" data-index-table-component-layout="mobile">
<% records.each do |record| %>
<div data-index-table-component-target="row"
class="card rounded-xl border border-base-300 bg-base-100 shadow-sm">
<div class="flex items-center justify-between rounded-t-xl border-b border-base-300 bg-base-200/70 px-4 py-3">
<div class="flex items-center gap-2">
<span class="text-xs font-semibold uppercase tracking-wide text-base-content/60">Actions</span>
</div>
<% if actions_column %>
<div class="flex items-center gap-2 text-base-content/70">
<%= cell_content_renderer.call(record, actions_column) %>
</div>
<% end %>
</div>
<div class="divide-y divide-base-200">
<% content_columns.each do |column| %>
<div class="flex items-start justify-between gap-4 px-4 py-3">
<div class="text-xs font-semibold uppercase tracking-wide text-base-content/45"><%= column.header %></div>
<div class="text-right text-sm text-base-content"><%= cell_content_renderer.call(record, column) %></div>
</div>
<% end %>
</div>
</div>
<% end %>
<div data-index-table-component-target="emptyRow"
class="card rounded-xl border border-dashed border-base-300 bg-base-200/40 hidden">
<div class="flex items-center justify-center p-6">
<div class="text-sm font-semibold text-base-content/50"><%= empty_state_text %></div>
</div>
</div>
<div class="flex justify-center pt-2">
<div class="join hidden" data-index-table-component-target="pagination">
<button class="join-item btn btn-sm btn-outline border-base-300 bg-base-100"
data-action="index-table-component#previousPage"
data-prev>
&lt;
</button>
<span class="join-item btn btn-sm border-base-300 bg-base-200 text-base-content/70 no-animation pointer-events-none"
data-index-table-component-target="pageInfo"></span>
<button class="join-item btn btn-sm btn-outline border-base-300 bg-base-100"
data-action="index-table-component#nextPage"
data-next>
&gt;
</button>
</div>
</div>
</div>
29 changes: 29 additions & 0 deletions app/components/shared/index_table_component/mobile_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module Shared
class IndexTableComponent
class MobileComponent < ViewComponent::Base
attr_reader :records, :columns, :empty_state_text, :cell_content_renderer

def initialize(records:, columns:, empty_state_text:, cell_content_renderer:)
super()
@records = records
@columns = columns
@empty_state_text = empty_state_text
@cell_content_renderer = cell_content_renderer
end

def content_columns
columns.reject { |column| action_column?(column) }
end

def actions_column
columns.find { |column| action_column?(column) }
end

private

def action_column?(column)
column.attribute.to_sym == :actions
end
end
Comment on lines +1 to +27
end
end
121 changes: 68 additions & 53 deletions app/javascript/controllers/index_table_component_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,15 @@
static values = { perPage: { type: Number, default: 10 } };

connect() {
this._rows = this.rowTargets || [];
this._filteredRows = [...this._rows];
this._query = "";
this._page = 0;
this._paginate();
this.refreshLayout();
}

filter() {
const query = (this.searchTarget?.value || "").trim().toLowerCase();

if (!query) {
this._filteredRows = [...this._rows];
} else {
this._filteredRows = this._rows.filter((row) => {
const text = row.textContent.replace(/\s+/g, " ").toLowerCase();
return text.includes(query);
});
}

this._query = (this.searchTarget?.value || "").trim().toLowerCase();
this._page = 0;
this._paginate();
this._refreshRows();
}

previousPage() {
Expand All @@ -41,8 +30,35 @@
}
}

refreshLayout() {
this._refreshRows();
}

// --- Private ---

_refreshRows() {
this._rows = this._activeRows();
this._filteredRows = this._filterRows(this._rows);
this._page = Math.min(this._page, this._lastPage());
this._paginate();
}

_activeRows() {
return this.rowTargets.filter((row) => {
const layout = row.closest("[data-index-table-component-layout]");
return layout && window.getComputedStyle(layout).display !== "none";
});
}

_filterRows(rows) {
if (!this._query) return [...rows];

return rows.filter((row) => {
const text = row.textContent.replace(/\s+/g, " ").toLowerCase();
return text.includes(this._query);
});
}

_lastPage() {
return Math.max(
0,
Expand All @@ -55,49 +71,48 @@
const start = this._page * perPage;
const end = (this._page + 1) * perPage;

this._rows.forEach((row) => (row.style.display = "none"));
this._filteredRows
.slice(start, end)
.forEach((row) => (row.style.display = ""));
this.rowTargets.forEach((row) => (row.style.display = "none"));
this._filteredRows.slice(start, end).forEach((row) => (row.style.display = ""));

Check failure on line 75 in app/javascript/controllers/index_table_component_controller.js

View workflow job for this annotation

GitHub Actions / lint_js_css

Replace `.slice(start,·end)` with `⏎······.slice(start,·end)⏎······`

// Toggle empty state
this.emptyRowTarget.classList.toggle(
"hidden",
this._filteredRows.length > 0,
);
this.emptyRowTargets.forEach((emptyRow) => {
const layout = emptyRow.closest("[data-index-table-component-layout]");
const isActive = layout && window.getComputedStyle(layout).display !== "none";

Check failure on line 79 in app/javascript/controllers/index_table_component_controller.js

View workflow job for this annotation

GitHub Actions / lint_js_css

Insert `⏎·······`
emptyRow.classList.toggle("hidden", !isActive || this._filteredRows.length > 0);

Check failure on line 80 in app/javascript/controllers/index_table_component_controller.js

View workflow job for this annotation

GitHub Actions / lint_js_css

Replace `"hidden",·!isActive·||·this._filteredRows.length·>·0` with `⏎········"hidden",⏎········!isActive·||·this._filteredRows.length·>·0,⏎······`
});

// Update pagination
if (this.hasPaginationTarget) {
this._updatePagination();
this._updatePagination(this._lastPage());
}
}

_updatePagination() {
const lastPage = this._lastPage();
const container = this.paginationTarget;

if (lastPage === 0) {
container.classList.add("hidden");
return;
}

container.classList.remove("hidden");

const prevBtn = container.querySelector("[data-prev]");
const nextBtn = container.querySelector("[data-next]");

if (prevBtn) {
prevBtn.disabled = this._page === 0;
prevBtn.classList.toggle("btn-disabled", this._page === 0);
}

if (nextBtn) {
nextBtn.disabled = this._page === lastPage;
nextBtn.classList.toggle("btn-disabled", this._page === lastPage);
}

if (this.hasPageInfoTarget) {
this.pageInfoTarget.textContent = `${this._page + 1} of ${lastPage + 1}`;
}
_updatePagination(lastPage) {
this.paginationTargets.forEach((container, index) => {
const layout = container.closest("[data-index-table-component-layout]");
const isActive = layout && window.getComputedStyle(layout).display !== "none";

Check failure on line 91 in app/javascript/controllers/index_table_component_controller.js

View workflow job for this annotation

GitHub Actions / lint_js_css

Insert `⏎·······`

if (!isActive || lastPage === 0) {
container.classList.add("hidden");
} else {
container.classList.remove("hidden");
}

const prevBtn = container.querySelector("[data-prev]");
const nextBtn = container.querySelector("[data-next]");

if (prevBtn) {
prevBtn.disabled = this._page === 0;
prevBtn.classList.toggle("btn-disabled", this._page === 0);
}

if (nextBtn) {
nextBtn.disabled = this._page === lastPage;
nextBtn.classList.toggle("btn-disabled", this._page === lastPage);
}

const pageInfo = this.pageInfoTargets[index];
if (pageInfo) {
pageInfo.textContent = `${this._page + 1} of ${lastPage + 1}`;
}
Comment on lines +88 to +115
});
}
}
Loading