Drag and drop cards to move between doing/considering
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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" } })
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -27,6 +27,7 @@ Rails.application.routes.draw do
|
||||
|
||||
namespace :cards do
|
||||
resources :previews
|
||||
resources :drops
|
||||
end
|
||||
|
||||
resources :cards do
|
||||
|
||||
Reference in New Issue
Block a user