Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 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:
- rails, rails-html-sanitizer, 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 LinkedListMoveValidator
include ProjectScoped

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
35 changes: 35 additions & 0 deletions app/controllers/concerns/linked_list_move_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
module LinkedListMoveValidator
extend ActiveSupport::Concern

protected

def set_prev_item_and_next_item
@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
unless valid_move_params?
redirect_to project_board_path(current_project, @board), alert: 'Something fishy is going on...'
end
end

private

def valid_move_params?
if @prev_item.present?
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 moveable_parent.items.empty?
@next_item.nil?
else
@next_item == moveable_parent.first_item
end
end
end
end
45 changes: 0 additions & 45 deletions app/controllers/concerns/validate_move.rb

This file was deleted.

44 changes: 44 additions & 0 deletions app/controllers/lists/position_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
class Lists::PositionController < AuthenticatedController
include ActivityTracking
include LinkedListMoveValidator
include ProjectScoped

before_action :set_current_board
before_action :set_list
before_action :set_prev_item_and_next_item
before_action :validate_move_params

def create
Board.move(@list, prev_item: @prev_item, next_item: @next_item)

track_updated(@list)

render json: @list
end

private

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

def moveable_item_name
'list'
end

def moveable_items
@board.lists
end

def moveable_parent
@board
end

def set_current_board
@board = current_project.boards.find(params[:board_id])
end

def set_list
@list = @board.lists.find(params[:list_id])
end
end
18 changes: 1 addition & 17 deletions app/controllers/lists_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ class ListsController < AuthenticatedController
include ProjectScoped

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_list, only: [:edit, :update, :destroy]

def new
@list = @board.lists.new
Expand Down Expand Up @@ -35,14 +32,6 @@ def update
end
end

def move
Board.move(@list, prev_item: @prev_item, next_item: @next_item)

track_updated(@list)

render json: @list
end

def destroy
if @list.destroy
track_destroyed(@list)
Expand All @@ -58,11 +47,6 @@ def list_params
params.require(:list).permit(:name)
end

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

def set_current_board
@board = current_project.boards.find(params[:board_id])
end
Expand Down
Loading
Loading