Drag and drop cards to move between doing/considering

This commit is contained in:
Jorge Manrubia
2025-06-12 11:57:53 +02:00
parent 65d9ef25b4
commit f58aaadba5
10 changed files with 171 additions and 18 deletions
+13
View File
@@ -0,0 +1,13 @@
@layer components {
.drag-and-drop__dragged-item {
border: 2px solid #e0e7ff;
opacity: 0.7;
box-shadow: 0 4px 6px rgba(99, 102, 241, 0.15);
}
.drag-and-drop__hover-container {
border: 2px dashed #818cf8;
background-color: #f5f3ff;
transition: all 0.2s ease;
}
}
+46
View File
@@ -0,0 +1,46 @@
class Cards::DropsController < ApplicationController
before_action :set_filter, :set_card, :set_drop_target
def create
case @drop_target
when :considering
@card.reconsider
when :doing
@card.engage
end
render_column_replacement
end
private
VALID_DROP_TARGETS = %w[ considering doing ]
def set_filter
@filter = Current.user.filters.from_params params.reverse_merge(**FilterScoped::DEFAULT_PARAMS).permit(*Filter::PERMITTED_PARAMS)
end
def set_card
@card = Current.user.accessible_cards.find(params[:dropped_item_id])
end
def set_drop_target
if params[:drop_target].in?(VALID_DROP_TARGETS)
@drop_target = params[:drop_target].to_sym
else
head :bad_request
end
end
def render_column_replacement
page_and_filter = page_and_filter_for @filter.with(engagement_status: @drop_target.to_s), per_page: CardsController::PAGE_SIZE
render turbo_stream: turbo_stream.replace("#{@drop_target}-cards", partial: "cards/index/engagement/#{@drop_target}", locals: page_and_filter.to_h)
end
def page_and_filter_for(filter, per_page: nil)
cards = block_given? ? yield(filter.cards) : filter.cards
OpenStruct.new \
page: GearedPagination::Recordset.new(cards, per_page:).page(1),
filter: filter
end
end
@@ -17,8 +17,7 @@ class Public::Collections::CardPreviewsController < ApplicationController
when "closed-cards"
@collection.cards.closed.recently_closed_first
else
head :bad_request
Card.none
raise ActionController::BadRequest
end
end
end
+2
View File
@@ -5,3 +5,5 @@ import "controllers"
import "actiontext-lexical"
import "@rails/actiontext"
window.fetch = Turbo.fetch // TODO: We need to make @rails/request.js use Turbo's fetch when it's present.
@@ -0,0 +1,81 @@
import { Controller } from "@hotwired/stimulus"
import { post } from "@rails/request.js"
import { nextFrame } from "helpers/timing_helpers"
export default class extends Controller {
static targets = [ "item", "container" ]
static values = { url: String }
static classes = [ "draggedItem", "hoverContainer" ]
// Actions
async dragStart(event) {
event.dataTransfer.effectAllowed = "move"
event.dataTransfer.dropEffect = "move"
event.dataTransfer.setData("37ui/move", event.target)
await nextFrame()
this.dragItem = this.#itemContaining(event.target)
this.sourceContainer = this.#containerContaining(this.dragItem)
this.dragItem.classList.add(this.draggedItemClass)
}
dragOver(event) {
event.preventDefault()
const container = this.#containerContaining(event.target)
this.#clearContainerHoverClasses()
if (!container) { return }
if (container !== this.sourceContainer) {
container.classList.add(this.hoverContainerClass)
}
}
async drop(event) {
const container = this.#containerContaining(event.target)
if (!container) { return }
await this.#submitDropRequest(this.dragItem, container)
this.wasDropped = true
}
dragEnd() {
this.dragItem.classList.remove(this.draggedItemClass)
this.#clearContainerHoverClasses()
if (this.wasDropped) {
this.dragItem.remove()
}
this.sourceContainer = null
this.dragItem = null
this.wasDropped = false
}
#itemContaining(element) {
return this.itemTargets.find(item => item.contains(element) || item === element)
}
#containerContaining(element) {
return this.containerTargets.find(container => container.contains(element) || container === element)
}
#clearContainerHoverClasses() {
this.containerTargets.forEach(container => container.classList.remove(this.hoverContainerClass))
}
// Private
async #submitDropRequest(item, container) {
const body = new FormData()
const id = item.dataset.id
const containerTarget = container.dataset.dropTarget
body.append("dropped_item_id", id)
body.append("drop_target", containerTarget)
return post(this.urlValue, { body, headers: { Accept: "text/vnd.turbo-stream.html" } })
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
<% cache cacheable_preview_parts_for(card) do %>
<%= card_article_tag card, class: "card" do %>
<%= card_article_tag card, class: "card", draggable: true, data: { id: card.id, drag_and_drop_target: "item" } do %>
<div class="flex gap">
<div class="flex flex-column flex-item-grow">
<header class="card__header">
@@ -13,7 +13,7 @@
<%= card.title %>
</h1>
<%= link_to collection_card_path(card.collection, card), class: "card__link", title: card_title_tag(card), data: { action: "dialog#close", turbo_frame: "_top" } do %>
<%= link_to collection_card_path(card.collection, card), draggable: false, class: "card__link", title: card_title_tag(card), data: { action: "dialog#close", turbo_frame: "_top" } do %>
<span class="for-screen-reader"><%= card.title %></span>
<% end %>
</div>
+16 -5
View File
@@ -13,9 +13,20 @@
<%= render "filters/settings", filter: @filter %>
</header>
<div class="card-columns">
<%= render "cards/index/engagement/considering", **@considering.to_h %>
<%= render "cards/index/engagement/doing", **@doing.to_h %>
</div>
<%= tag.div data: {
controller: "drag-and-drop",
drag_and_drop_dragged_item_class: "drag-and-drop__dragged-item",
drag_and_drop_hover_container_class: "drag-and-drop__hover-container",
drag_and_drop_url_value: cards_drops_path(**@filter.as_params),
action: "
dragstart->drag-and-drop#dragStart
dragover->drag-and-drop#dragOver
drop->drag-and-drop#drop
dragend->drag-and-drop#dragEnd" } do %>
<div class="card-columns">
<%= render "cards/index/engagement/considering", **@considering.to_h %>
<%= render "cards/index/engagement/doing", **@doing.to_h %>
</div>
<%= render "cards/index/engagement/closed", **@closed.to_h %>
<%= render "cards/index/engagement/closed", **@closed.to_h %>
<% end %>
@@ -1,4 +1,4 @@
<section id="considering-cards" class="cards cards--considering">
<section id="considering-cards" class="cards cards--considering" data-drag-and-drop-target="container" data-drop-target="considering">
<h2 class="cards__heading">
Considering
</h2>
@@ -1,4 +1,4 @@
<section id="doing-cards" class="cards cards--doing" style="view-transition-name: cards-container;">
<section id="doing-cards" class="cards cards--doing" style="view-transition-name: cards-container;" data-drag-and-drop-target="container" data-drop-target="doing">
<% if workflow = filter.single_workflow %>
<%= render "cards/index/workflow_filter", workflow: workflow, filter: filter %>
<% else %>
@@ -7,13 +7,13 @@
</h2>
<% end %>
<% if page.used? %>
<%= render partial: "cards/display/preview", collection: page.records, as: :card, cached: ->(card) { cacheable_preview_parts_for(card) } %>
<% if page.used? %>
<%= render partial: "cards/display/preview", collection: page.records, as: :card, cached: ->(card) { cacheable_preview_parts_for(card) } %>
<% unless page.last? %>
<%= cards_next_page_link "doing-cards", page: page, filter: filter %>
<% unless page.last? %>
<%= cards_next_page_link "doing-cards", page: page, filter: filter %>
<% end %>
<% else %>
<p class="txt-medium translucent">Nothing here</p>
<% end %>
<% else %>
<p class="txt-medium translucent">Nothing here</p>
<% end %>
</section>
+1
View File
@@ -27,6 +27,7 @@ Rails.application.routes.draw do
namespace :cards do
resources :previews
resources :drops
end
resources :cards do