Skip to content

Commit

Permalink
Merge pull request #5522 from nebulab/rainerd/admin/add-sortable-lists
Browse files Browse the repository at this point in the history
[Admin] Add sortable rows in `ui/table` component
  • Loading branch information
rainerdema committed Nov 29, 2023
2 parents 9609689 + a0b8a40 commit 0bbb702
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 95 deletions.
131 changes: 71 additions & 60 deletions admin/app/components/solidus_admin/ui/table/component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,77 +7,80 @@
data-controller="<%= stimulus_id %>"
data-<%= stimulus_id %>-selected-row-class="bg-gray-15"
data-<%= stimulus_id %>-mode-value="<%= initial_mode %>"
data-<%= stimulus_id %>-sortable-value="<%= @sortable.present? %>"
data-action="
<%= component("ui/table/ransack_filter").stimulus_id %>:search-><%= stimulus_id %>#search
<%= component("ui/table/ransack_filter").stimulus_id %>:showSearch-><%= stimulus_id %>#showSearch
"
>
<div role="search">
<%= render component("ui/table/toolbar").new("data-#{stimulus_id}-target": "searchToolbar", hidden: initial_mode != "search") do %>
<%= form_with(
url: @search.url,
method: :get,
html: {
id: search_form_id,
class: 'flex-grow',
"data-turbo-action": "replace",
"data-#{stimulus_id}-target": "searchForm",
"data-action": "input->#{stimulus_id}#search change->#{stimulus_id}#search",
},
) do |form| %>
<%= hidden_field_tag @search.scope_param_name, @search.current_scope.name if @search.scopes.present? %>
<%= render component('ui/forms/search_field').new(
name: @search.searchbar_param_name,
value: @search.value[@search.searchbar_key],
placeholder: t('.search_placeholder', resources: @data.plural_name),
"aria-label": t('.search_placeholder', resources: @data.plural_name),
"data-#{stimulus_id}-target": "searchField",
"data-turbo-permanent": "true",
id: "#{stimulus_id}-search-field-#{@id}",
) %>
<% end %>
<% if @search %>
<div role="search">
<%= render component("ui/table/toolbar").new("data-#{stimulus_id}-target": "searchToolbar", hidden: initial_mode != "search") do %>
<%= form_with(
url: @search.url,
method: :get,
html: {
id: search_form_id,
class: 'flex-grow',
"data-turbo-action": "replace",
"data-#{stimulus_id}-target": "searchForm",
"data-action": "input->#{stimulus_id}#search change->#{stimulus_id}#search",
},
) do |form| %>
<%= hidden_field_tag @search.scope_param_name, @search.current_scope.name if @search.scopes.present? %>
<%= render component('ui/forms/search_field').new(
name: @search.searchbar_param_name,
value: @search.value[@search.searchbar_key],
placeholder: t('.search_placeholder', resources: @data.plural_name),
"aria-label": t('.search_placeholder', resources: @data.plural_name),
"data-#{stimulus_id}-target": "searchField",
"data-turbo-permanent": "true",
id: "#{stimulus_id}-search-field-#{@id}",
) %>
<% end %>

<div class="ml-4">
<%= render component("ui/button").new(
text: t('.cancel'),
scheme: :ghost,
"data-action": "#{stimulus_id}#cancelSearch",
) %>
</div>
<% end %>
<div class="ml-4">
<%= render component("ui/button").new(
text: t('.cancel'),
scheme: :ghost,
"data-action": "#{stimulus_id}#cancelSearch",
) %>
</div>
<% end %>
<% if @search.filters.any? %>
<%= render component("ui/table/toolbar").new("data-#{stimulus_id}-target": "filterToolbar", hidden: initial_mode != "search") do %>
<% @search.filters.each_with_index do |filter, index| %>
<%= render_ransack_filter_dropdown(filter, index) %>
<% if @search.filters.any? %>
<%= render component("ui/table/toolbar").new("data-#{stimulus_id}-target": "filterToolbar", hidden: initial_mode != "search") do %>
<% @search.filters.each_with_index do |filter, index| %>
<%= render_ransack_filter_dropdown(filter, index) %>
<% end %>
<% end %>
<% end %>
<% end %>
<%= render component("ui/table/toolbar").new("data-#{stimulus_id}-target": "scopesToolbar", hidden: initial_mode != "scopes") do %>
<div class="flex-grow">
<%= form_with(url: @search.url, method: :get) do %>
<% @search.scopes.each do |scope| %>
<%= render component("ui/tab").new(
tag: :button,
type: :submit,
text: scope.label,
current: scope == @search.current_scope,
name: @search.scope_param_name,
value: scope.name,
) %>
<%= render component("ui/table/toolbar").new("data-#{stimulus_id}-target": "scopesToolbar", hidden: initial_mode != "scopes") do %>
<div class="flex-grow">
<%= form_with(url: @search.url, method: :get) do %>
<% @search.scopes.each do |scope| %>
<%= render component("ui/tab").new(
tag: :button,
type: :submit,
text: scope.label,
current: scope == @search.current_scope,
name: @search.scope_param_name,
value: scope.name,
) %>
<% end %>
<% end %>
<% end %>
</div>
</div>

<%= render component("ui/button").new(
'aria-label': t('.filter'),
icon: "filter-3-line",
scheme: :secondary,
"data-action": "#{stimulus_id}#showSearch",
) %>
<% end %>
</div>
<%= render component("ui/button").new(
'aria-label': t('.filter'),
icon: "filter-3-line",
scheme: :secondary,
"data-action": "#{stimulus_id}#showSearch",
) %>
<% end %>
</div>
<% end %>
<%= render component("ui/table/toolbar").new("data-#{stimulus_id}-target": "batchToolbar", role: "toolbar", "aria-label": t(".batch_actions"), hidden: true) do %>
<%= form_tag '', id: batch_actions_form_id %>
Expand Down Expand Up @@ -120,13 +123,21 @@
</thead>
<% end %>

<tbody class="bg-white text-3.5 line-[150%] text-black">
<tbody
class="bg-white text-3.5 line-[150%] text-black"
data-<%= stimulus_id %>-target="tableBody"
<%= "data-controller=sortable" if @sortable&.url %>
<%= "data-sortable-param-value=#{@sortable.param}" if @sortable&.param %>
<%= "data-sortable-handle-value=#{@sortable.handle}" if @sortable&.handle %>
<%= "data-sortable-animation-value=#{@sortable.animation}" if @sortable&.animation %>
>
<% @data.rows.each do |row| %>
<tr
class="border-b border-gray-100 last:border-0 hover:bg-gray-50 cursor-pointer <%= 'bg-gray-15 text-gray-700' if @data.fade&.call(row) %>"
<% if @data.url %>
data-action="click-><%= stimulus_id %>#rowClicked"
data-<%= stimulus_id %>-url-param="<%= @data.url.call(row) %>"
<%= "data-sortable-url=#{@sortable.url.call(row)}" if @sortable&.url %>
<% end %>
>
<% @data.columns.each do |column| %>
Expand Down
21 changes: 17 additions & 4 deletions admin/app/components/solidus_admin/ui/table/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ export default class extends Controller {
"filterToolbar",
"defaultHeader",
"batchHeader",
"tableBody",
"selectedRowsCount",
]

static classes = ["selectedRow"]
static values = {
mode: { type: String, default: "scopes" },
sortable: { type: Boolean, default: false },
}

initialize() {
Expand Down Expand Up @@ -62,7 +64,7 @@ export default class extends Controller {
selectRow(event) {
if (this.checkboxTargets.some((checkbox) => checkbox.checked)) {
this.modeValue = "batch"
} else if (this.searchFieldTarget.value !== '') {
} else if (this.hasSearchFieldTarget && (this.searchFieldTarget.value !== '')) {
this.modeValue = "search"
} else {
this.modeValue = "scopes"
Expand All @@ -74,7 +76,7 @@ export default class extends Controller {
selectAllRows(event) {
if (event.target.checked) {
this.modeValue = "batch"
} else if (this.searchFieldTarget.value !== '') {
} else if (this.hasSearchFieldTarget && (this.searchFieldTarget.value !== '')) {
this.modeValue = "search"
} else {
this.modeValue = "scopes"
Expand Down Expand Up @@ -108,7 +110,9 @@ export default class extends Controller {
render() {
const selectedRows = this.checkboxTargets.filter((checkbox) => checkbox.checked)

this.searchToolbarTarget.toggleAttribute("hidden", this.modeValue !== "search")
if (this.hasSearchFieldTarget) {
this.searchToolbarTarget.toggleAttribute("hidden", this.modeValue !== "search")
}

if (this.hasFilterToolbarTarget) {
this.filterToolbarTarget.toggleAttribute("hidden", this.modeValue !== "search")
Expand All @@ -118,13 +122,22 @@ export default class extends Controller {
this.batchHeaderTarget.toggleAttribute("hidden", this.modeValue !== "batch")
this.defaultHeaderTarget.toggleAttribute("hidden", this.modeValue === "batch")

this.scopesToolbarTarget.toggleAttribute("hidden", this.modeValue !== "scopes")
if (this.hasScopesToolbarTarget) {
this.scopesToolbarTarget.toggleAttribute("hidden", this.modeValue !== "scopes")
}

// Update the rows background color
this.checkboxTargets.filter((checkbox) =>
checkbox.closest("tr").classList.toggle(this.selectedRowClass, checkbox.checked),
)

// Determine if sortable should be enabled
if (this.sortableValue && this.modeValue !== "batch" && this.modeValue !== "search") {
this.tableBodyTarget.setAttribute('data-controller', 'sortable');
} else {
this.tableBodyTarget.removeAttribute('data-controller');
}

// Update the selected rows count
this.selectedRowsCountTarget.textContent = `${selectedRows.length}`

Expand Down
10 changes: 6 additions & 4 deletions admin/app/components/solidus_admin/ui/table/component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ class SolidusAdmin::UI::Table::Component < SolidusAdmin::BaseComponent
Column = Struct.new(:header, :data, :col, :wrap, keyword_init: true)
Filter = Struct.new(:presentation, :combinator, :attribute, :predicate, :options, keyword_init: true)
Scope = Struct.new(:name, :label, :default, keyword_init: true)
private_constant :BatchAction, :Column, :Filter, :Scope
Sortable = Struct.new(:url, :param, :animation, :handle, keyword_init: true)
private_constant :BatchAction, :Column, :Filter, :Scope, :Sortable

class Data < Struct.new(:rows, :class, :url, :prev, :next, :columns, :fade, :batch_actions, keyword_init: true) # rubocop:disable Lint/StructNewOverride,Style/StructInheritance
def initialize(**args)
Expand Down Expand Up @@ -49,11 +50,12 @@ def value
end
end

def initialize(id:, data:, search: nil)
def initialize(id:, data:, search: nil, sortable: nil)
@id = id
@data = Data.new(**data)
@data.columns.unshift selectable_column if @data.batch_actions.present?
@search = Search.new(**search)
@search = Search.new(**search) if search
@sortable = Sortable.new(**sortable) if sortable
end

def selectable_column
Expand Down Expand Up @@ -152,6 +154,6 @@ def current_scope_name
end

def initial_mode
@initial_mode ||= @search.value[@search.searchbar_key] ? "search" : "scopes"
@initial_mode ||= @search && @search.value[@search.searchbar_key] ? "search" : "scopes"
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Controller } from "@hotwired/stimulus"
import Sortable from "sortablejs"
import { patch } from '@rails/request.js'

export default class extends Controller {
static values = {
param: { type: String, default: 'postion' },
handle: { type: String, default: null },
animation: { type: Number, default: 150 },
}

connect() {
this.sortable = Sortable.create(this.element, {
onEnd: this.onEnd.bind(this),
animation: this.animationValue,
handle: this.handleValue,
})
}

async onEnd({ item, newIndex }) {
if (!item.dataset.sortableUrl) return

const data = new FormData()
data.append(this.paramValue, newIndex + 1)

return await patch(item.dataset.sortableUrl, { body: data, responseKind: "js" })
}

disconnect() {
this.sortable.destroy()
this.sortable = null
}
}
2 changes: 2 additions & 0 deletions admin/config/importmap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
pin "@hotwired/turbo-rails", to: "turbo.js"

pin "stimulus-use", to: "https://ga.jspm.io/npm:[email protected]/dist/index.js"
pin "sortablejs", to: "https://cdn.jsdelivr.net/npm/[email protected]/+esm"
pin "@rails/request.js", to: "https://cdn.jsdelivr.net/npm/@rails/[email protected]/+esm"

pin "solidus_admin/application", preload: true
pin "solidus_admin/utils"
Expand Down
Loading

0 comments on commit 0bbb702

Please sign in to comment.