Skip to content

Commit

Permalink
Add orderable tables
Browse files Browse the repository at this point in the history
First version adds support for drag-and-drop. Styling needs more work.
  • Loading branch information
sfnelson committed Sep 20, 2023
1 parent 17fbbd2 commit c30b203
Show file tree
Hide file tree
Showing 14 changed files with 387 additions and 12 deletions.
4 changes: 2 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
katalyst-koi (4.0.3)
katalyst-koi (4.1.0)
bcrypt
importmap-rails
katalyst-content
Expand Down Expand Up @@ -184,7 +184,7 @@ GEM
redlock (>= 1.2)
katalyst-kpop (2.0.9)
katalyst-navigation (1.4.1)
katalyst-tables (2.2.1)
katalyst-tables (2.2.2)
html-attributes-utils
view_component
language_server-protocol (3.17.0.3)
Expand Down
11 changes: 5 additions & 6 deletions app/assets/stylesheets/koi/components/_index-table.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@use "index-table/ordinal" as *;

@mixin sort-icon {
display: inline-block;
content: " ";
Expand Down Expand Up @@ -54,13 +56,10 @@ $row-height: 48px !default;
text-decoration: none;
}

> img {
width: 6rem;
padding: 1rem 0;
}

> img,
> a > img {
padding-top: 1rem;
max-height: 3rem;
padding: 0;
}
}

Expand Down
21 changes: 21 additions & 0 deletions app/assets/stylesheets/koi/components/index-table/_ordinal.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
$width: 2rem !default;

.index-table {
th.ordinal {
width: $width;
padding-left: 0;
a {
width: $width;
height: 3rem;
}
a::after {
right: 0;
}
}

td.ordinal {
width: $width;
padding-left: 0;
cursor: grab;
}
}
27 changes: 27 additions & 0 deletions app/components/koi/ordinal_table_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module Koi
class OrdinalTableComponent < Katalyst::Turbo::TableComponent
include Katalyst::Tables::Orderable

def initialize(collection:,
id: "index-table",
url: [:order, :admin, collection],
scope: "order[#{collection.model_name.plural}]",
**options)
super(collection:, id:, class: "index-table", caption: true, **options)

@url = url
@scope = scope
end

def before_render
with_orderable(url: @url, scope: @scope) unless orderable?
end

def call
concat(render_parent)
concat(orderable)
end
end
end
17 changes: 14 additions & 3 deletions lib/generators/koi/active_record/active_record_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ def admin_search
"PgSearch::Model".safe_constantize ? pg_search : sql_search
end

def ordinal_scope
return unless attributes.any? { |attr| attr.name == "ordinal" }

insert_into_file "app/models/#{file_name}.rb", before: /end\Z/ do
<<~RUBY
default_scope -> { order(ordinal: :asc) }
RUBY
end
end

private

def pg_search
Expand All @@ -19,18 +29,19 @@ def pg_search
RUBY
end

insert_into_file "app/models/#{file_name}.rb", before: "end\n" do
insert_into_file "app/models/#{file_name}.rb", before: /end\Z/ do
<<~RUBY
pg_search_scope :admin_search, against: %i[#{search_fields.join(' ')}], using: { tsearch: { prefix: true } }
RUBY
end
end

def sql_search
insert_into_file "app/models/#{file_name}.rb", before: "end\n" do
insert_into_file "app/models/#{file_name}.rb", before: /end\Z/ do
clause = search_fields.map { |f| "#{f} LIKE :query" }.join(" OR ")
<<~RUBY
scope :admin_search, ->(query) do
where("#{search_fields.map { |f| "#{f} LIKE :query" }.join(' OR ')}", query: "%\#{query}%")
where("#{clause}", query: "%\#{query}%")
end
RUBY
end
Expand Down
1 change: 1 addition & 0 deletions lib/generators/koi/admin/admin_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ class AdminGenerator < Rails::Generators::ScaffoldGenerator
hook_for :admin_controller, in: :koi, as: :admin, type: :boolean, default: true

Rails::Generators::ModelGenerator.hook_for :admin_search, type: :boolean, default: true
Rails::Generators::ModelGenerator.hook_for :ordinal_scope, type: :boolean, default: true
end
end
2 changes: 1 addition & 1 deletion lib/koi/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Koi
VERSION = "4.0.3"
VERSION = "4.1.0"
end
7 changes: 7 additions & 0 deletions lib/tasks/dummy.thor
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,18 @@ class Dummy < Thor
inside("spec/dummy") do
run <<~SH
rails g koi:admin Post name:string title:string content:rich_text active:boolean published_on:date
rails g koi:admin Banner name:string image:attachment ordinal:integer
SH

run "rails db:migrate"
end

gsub_file("config/routes/admin.rb", "resources :banners\n", <<~RUBY)
resources :banners do
patch :order, on: :collection
end
RUBY

Dir.glob(File.join(self.class.source_root, "**/*")).each do |file|
copy_file(file[(self.class.source_root.size + 1)..], force: true) if File.file?(file)
end
Expand Down
13 changes: 13 additions & 0 deletions spec/support/capybara.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

require "capybara/rspec"

module WaitForTurbo
def wait_for_form_submission
Timeout.timeout(Capybara.default_max_wait_time) do
loop until form_submission_complete?
end
end

def form_submission_complete?
page.evaluate_script("Turbo.navigator.formSubmission.state === 5")
end
end

RSpec.configure do |config|
config.include Capybara::RSpecMatchers, type: :request
config.include WaitForTurbo, type: :system
end
85 changes: 85 additions & 0 deletions spec/support/templates/app/controllers/admin/banners_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# frozen_string_literal: true

module Admin
class BannersController < ApplicationController
before_action :set_banner, only: %i[show edit update destroy]

def index
collection = Collection.new.with_params(params).apply(::Banner.strict_loading)
table = Koi::OrdinalTableComponent.new(collection:)

respond_to do |format|
format.turbo_stream { render table } if self_referred?
format.html { render :index, locals: { table:, collection: } }
end
end

def show
render locals: { banner: @banner }
end

def new
render locals: { banner: ::Banner.new }
end

def edit
render locals: { banner: @banner }
end

def create
@banner = ::Banner.new(banner_params)

if @banner.save
redirect_to [:admin, @banner]
else
render :new, locals: { banner: @banner }, status: :unprocessable_entity
end
end

def update
if @banner.update(banner_params)
redirect_to action: :show
else
render :edit, locals: { banner: @banner }, status: :unprocessable_entity
end
end

def destroy
@banner.destroy

redirect_to action: :index
end

def order
order_params[:banners].each do |id, attrs|
Banner.find(id).update(attrs)
end

redirect_back(fallback_location: admin_banners_path, status: :see_other)
end

private

# Only allow a list of trusted parameters through.
def banner_params
params.require(:banner).permit(:name, :image, :ordinal)
end

def order_params
params.require(:order).permit(banners: [:ordinal])
end

# Use callbacks to share common setup or constraints between actions.
def set_banner
@banner = ::Banner.find(params[:id])
end

class Collection < Katalyst::Tables::Collection::Base
attribute :search, :string

def filter
self.items = items.admin_search(search) if search.present?
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<% row.ordinal %>
<% row.cell :name do |cell| %>
<%= link_to cell.value, url_for([:admin, banner]) %>
<% end %>
8 changes: 8 additions & 0 deletions spec/support/templates/spec/factories/banners.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

FactoryBot.define do
factory :banner do
name { Faker::Kpop.solo }
sequence(:ordinal)
end
end
Loading

0 comments on commit c30b203

Please sign in to comment.