Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
941aa68
Add move to list option in card
nicolachr Apr 30, 2026
abf51e4
Add specs
nicolachr Apr 30, 2026
d67902a
Add CHANGELOG entry
nicolachr Apr 30, 2026
7b8fab7
Move inline with other actions
nicolachr May 7, 2026
e682e68
Merge branch 'develop' into cards/add-list-dropdown
nicolachr May 7, 2026
4b709e4
Address APR findings
nicolachr May 7, 2026
746bb51
track card list moves in the activity feed
nicolachr May 12, 2026
c9e6ed0
move list filtering out of the card actions partial
nicolachr May 12, 2026
92acb8e
Merge branch 'develop' into cards/add-list-dropdown
nicolachr May 28, 2026
14aebeb
Merge branch 'develop' into cards/add-list-dropdown
nicolachr May 28, 2026
fa11864
Show all lists in dropdown, disable card's current list
nicolachr Jun 4, 2026
8c59b71
Scope card show cache key to card's list
nicolachr Jun 4, 2026
b4264cb
Track move_to_list as an update activity
nicolachr Jun 4, 2026
87511f2
Merge branch 'develop' into cards/add-list-dropdown
nicolachr Jun 4, 2026
1b2ed24
Merge branch 'develop' into cards/add-list-dropdown
nicolachr Jun 18, 2026
12944aa
Extract card move actions into sub-resource controllers
nicolachr Jun 18, 2026
ef8960f
Fix cache invalidation and atomic save in card position/transfer
nicolachr Jun 18, 2026
17cc212
Remove unused Eventable from Card model
nicolachr Jun 19, 2026
eb91548
Merge branch 'develop' into cards/add-list-dropdown
nicolachr Jul 10, 2026
458f24e
Remove transfer controller
aapomm Jul 15, 2026
651c2c9
Update the positions controller to use EventPublisher
aapomm Jul 15, 2026
0c1187a
Merge branch 'develop' into cards/add-list-dropdown
aapomm Jul 15, 2026
77992f9
Merge branch 'develop' into cards/add-list-dropdown
aapomm Aug 3, 2026
42135fd
Add PositionsController for Lists and rename concern
aapomm Aug 3, 2026
f782d5b
Simplify cache key and use link_to_if
aapomm Aug 3, 2026
73e3e33
Add back payload data and use :update for positions controller
aapomm Aug 4, 2026
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
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[v#.#.#] ([month] [YYYY])
- [entity]:
- [future tense verb] [feature]
- Cards: add option to move a task to a different list
- Upgraded gems:
- sqlite3, websocket-driver
- Bugs fixes:
Expand Down
4 changes: 0 additions & 4 deletions app/assets/stylesheets/hera/modules.scss
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,6 @@
margin-left: auto;

.action {
&:not(:first-child)::before {
content: '-';
}

&:first-child {
margin-left: auto;
}
Expand Down
6 changes: 6 additions & 0 deletions app/assets/stylesheets/hera/modules/_dropdown.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@
font-weight: 600;
}

&.disabled {
color: var(--text-muted);
cursor: not-allowed;
pointer-events: none;
}

i {
margin-right: 0.25rem;
}
Expand Down
107 changes: 107 additions & 0 deletions app/controllers/cards/position_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
class Cards::PositionController < AuthenticatedController
include EventPublisher
include ProjectScoped
include ValidateMove

before_action :set_current_board_and_list
before_action :set_card
before_action :require_list_change, if: :append_to_list?
before_action :set_prev_item_and_next_item
before_action :validate_move_params

def create
Card.transaction do
List.move(@card, prev_item: @prev_item, next_item: @next_item)

if new_list
@card.list = new_list
@card.save!
end
end

publish_event('card.updated', @card.to_event_payload)
@card.reload

respond_to do |format|
format.html do
redirect_to project_board_list_card_path(current_project, @board, @card.list, @card),
notice: 'Task moved.'
end

format.json do
render json: {
is_card: true,
id: @card.id,
link: polymorphic_path([current_project, @board, @card.list, @card]),
moveLink: project_board_list_card_position_path(current_project, @board, @card.list, @card)
}
end
end
end

private

# A request with a new_list_id but no explicit position appends the card to
# the end of the target list (e.g. the 'move to list' dropdown).
def append_to_list?
move_params[:new_list_id].present? &&
move_params[:prev_id].blank? &&
move_params[:next_id].blank?
end

# Override EventPublisher#event_action_payload: the RESTful action is
# 'create', but moving a card is semantically an update.
#
# FIXME: Replace with ActivityService action registration once
# convention-over-configuration approach is implemented.
def event_action_payload
super.merge(action: 'update')
end

def move_params
params.permit(:card_id, :project_id, :board_id, :list_id, :next_id, :prev_id, :new_list_id)
end

def moveable_item_name
'card'
end

def moveable_items
@board.cards
end

def moveable_parent
new_list || @list
end

def new_list
return if move_params[:new_list_id].blank?

@new_list ||= @board.lists.find(move_params[:new_list_id])
end

def require_list_change
return if new_list.id != @card.list_id

redirect_to project_board_list_card_path(current_project, @board, @list, @card),
alert: 'Task is already in that list.'
end

def set_card
@card = @board.cards.find(params[:card_id])
end

def set_current_board_and_list
@board = current_project.boards.includes(:lists).find(params[:board_id])
@list = @board.lists.includes(:cards).find(params[:list_id])
end

def set_prev_item_and_next_item
if append_to_list?
@prev_item = new_list.last_card
@next_item = nil
else
super
end
end
end
35 changes: 2 additions & 33 deletions app/controllers/cards_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,10 @@ class CardsController < AuthenticatedController
before_action :initialize_sidebar, only: [:show, :new, :edit]
before_action :set_auto_save_key, only: [:new, :create, :edit, :update]

# Not at top because we need board and list set first
include ValidateMove

layout 'cards'

def show
@lists = @board.ordered_lists
render layout: !request.xhr?
end

Expand Down Expand Up @@ -52,24 +50,6 @@ def update
end
end

def move
List.move(@card, prev_item: @prev_item, next_item: @next_item)

if new_list
@card.list = new_list
@card.save
end

track_updated(@card)

render json: {
is_card: true,
id: @card.id,
link: polymorphic_path([current_project, @board, @card.reload.list, @card]),
moveLink: move_project_board_list_card_path(current_project, @board, @card.reload.list, @card)
}
end

def destroy
if @card.destroy
track_destroyed(@card)
Expand All @@ -85,13 +65,6 @@ def card_params
params.require(:card).permit(:name, :description, :due_date, assignee_ids: [])
end

def move_params
params.
permit(:id, :project_id, :board_id, :list_id,
:next_id, :prev_id, :new_list_id
)
end

def initialize_sidebar
@sorted_cards = @list.ordered_cards.select(&:persisted?)
end
Expand All @@ -107,7 +80,7 @@ def set_or_initialize_card

def set_current_board_and_list
@board = current_project.boards.includes(:lists).find(params[:board_id])
@list = @board.lists.includes(:cards).find(params[:list_id])
@list = @board.lists.includes(:cards).find(params[:list_id])
end

def set_auto_save_key
Expand All @@ -119,8 +92,4 @@ def set_auto_save_key
"#{@list.id}-card"
end
end

def new_list
@board.lists.find(move_params[:new_list_id]) if move_params[:new_list_id]
end
end
22 changes: 6 additions & 16 deletions app/controllers/concerns/validate_move.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
module ValidateMove
Comment thread
aapomm marked this conversation as resolved.
Outdated
extend ActiveSupport::Concern
included do
before_action :set_prev_item_and_next_item, only: :move
before_action :validate_move_params, only: :move
end

protected

def set_prev_item_and_next_item
@prev_item = @board.send(controller_name).find_by(id: move_params[:prev_id])
@next_item = @board.send(controller_name).find_by(id: move_params[:next_id])
@prev_item = moveable_items.find_by(id: move_params[:prev_id])
@next_item = moveable_items.find_by(id: move_params[:next_id])
end

def validate_move_params
Expand All @@ -18,27 +14,21 @@ def validate_move_params
end
end

def parent
if controller_name == 'cards'
new_list || @list
elsif controller_name == 'lists'
@board
end
end
private

def valid_move_params?
if @prev_item.present?
next_item_of_prev_item = @prev_item.send("next_#{controller_name.classify.downcase}")
next_item_of_prev_item = @prev_item.send("next_#{moveable_item_name}")
if next_item_of_prev_item
@next_item == next_item_of_prev_item
else
@next_item.nil?
end
else
if parent.items.empty?
if moveable_parent.items.empty?
@next_item.nil?
else
@next_item == parent.first_item
@next_item == moveable_parent.first_item
end
end
end
Expand Down
18 changes: 15 additions & 3 deletions app/controllers/lists_controller.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
class ListsController < AuthenticatedController
include ActivityTracking
include ProjectScoped
include ValidateMove

before_action :set_current_board
before_action :set_list, only: [:edit, :update, :destroy, :move]

# Not at top because we need board set first
include ValidateMove
before_action :set_prev_item_and_next_item, only: :move
before_action :validate_move_params, only: :move

def new
@list = @board.lists.new
Expand Down Expand Up @@ -63,6 +63,18 @@ def move_params
permit(:id, :project_id, :board_id, :next_id, :prev_id)
end

def moveable_items
@board.lists
end

def moveable_item_name
'list'
end

def moveable_parent
@board
end

def set_current_board
@board = current_project.boards.find(params[:board_id])
end
Expand Down
13 changes: 12 additions & 1 deletion app/models/card.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
class Card < ApplicationRecord
include Commentable
include Eventable
include HasFields
include RevisionTracking
include Subscribable
Expand Down Expand Up @@ -84,9 +85,19 @@ def to_xml(xml_builder, includes: [], version: 3)
end
end

def local_event_payload

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're adding this for cards but are we actually using it? Either we keep this and use eventpublisher for cards, or leave this out for now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed for now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aapomm please go ahead and add it.

We probably need to include list information in the payload (and even a before/after in the particular Position one).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this resolved @aapomm ? I think that someone paying attention to this event would like List / Board?

{
name: name,
project: {
id: project.id,
name: project.name
}
}
end

def local_fields
{
'List' => list.name.parameterize(preserve_case: true, separator: '_'),
'List' => list.name.parameterize(preserve_case: true, separator: '_'),
'Title' => name
}
end
Expand Down
2 changes: 1 addition & 1 deletion app/views/boards/_board.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
<ul class="list-cards js-sortable-cards">
<% list.ordered_cards.each do |card| %>
<% cache ['board-list-card', card] do %>
<li class="card" data-card-id="<%= card.id %>" data-move-url="<%= move_project_board_list_card_path(current_project, board, list, card) %>">
<li class="card" data-card-id="<%= card.id %>" data-move-url="<%= project_board_list_card_position_path(current_project, board, list, card) %>">
<%= link_to project_board_list_card_path(current_project, board, list, card) do %>
<div class="card-details">
<div class="card-title"><%= card.name %></div>
Expand Down
20 changes: 20 additions & 0 deletions app/views/cards/_actions.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,24 @@
<div class="actions">
<% if @lists.count > 1 %>
<span class="action dropdown">
<a href="#" class="dropdown-toggle" data-bs-toggle="dropdown"><i class="fa-solid fa-table-columns fa-fw"></i> <%= @list.name %></a>
<div class="dropdown-menu">
<% @lists.each do |list| %>
<% if list.id == @list.id %>
<a href="javascript:void(0)" class="dropdown-item disabled"><%= list.name %></a>
<% else %>
<%= link_to(
project_board_list_card_position_path(current_project, @board, @list, @card, new_list_id: list.id),
method: :post,
class: 'dropdown-item'
) do %>
<%= list.name %>
<% end %>
<% end %>
<% end %>
</div>
</span>
<% end %>
<span class="action">
<%= link_to edit_project_board_list_card_path(current_project, @board, @list, @card) do %>
<i class="fa-solid fa-pencil fa-fw"></i> Edit
Expand Down
2 changes: 1 addition & 1 deletion app/views/cards/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<div class="content-container">
<div class="tab-content">
<div class="tab-pane active" id="info-tab">
<% cache ['card-information-tab', @card] do %>
<% cache ['card-information-tab', @card, @card.list, @board] do %>
Comment thread
aapomm marked this conversation as resolved.
Outdated
<div class="note-text-inner">
<h4 class="mb-4 header-underline">
<span class="text-truncate" title="<%= @card.name %>"><%= @card.name %></span>
Expand Down
1 change: 1 addition & 0 deletions config/initializers/activity_service.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
Rails.application.reloader.to_prepare do
ActivityService.configure do |activity_service|
activity_service.subscribe_namespace 'card'
activity_service.subscribe_namespace 'inline_thread'
activity_service.subscribe_namespace 'issue'
end
Expand Down
Loading
Loading