Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
- Mail: add support for SMTP configuration via environment variables for Docker deployments; smtp.yml remains supported for VM deployments during the deprecation transition
- Upgraded gems:
- [gem]
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 @@ -198,10 +198,6 @@
margin-left: auto;

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

&:first-child {
margin-left: auto;
}
Expand Down
25 changes: 25 additions & 0 deletions app/controllers/cards_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
class CardsController < AuthenticatedController
include ActivityTracking
include ContentFromTemplate
include EventPublisher
include Mentioned
include NotificationsReader
include ProjectScoped
Expand Down Expand Up @@ -52,6 +53,30 @@ def update
end
end

def move_to_list
Comment thread
nicolachr marked this conversation as resolved.
Outdated
target_list = @board.lists.find(params[:new_list_id])

if target_list.id == @card.list_id
redirect_to [current_project, @board, @list, @card], alert: 'Task is already in that list.'
return
end

Card.transaction do
# Repair the source list chain before changing list_id,
# since next_card scopes to self.list
if (next_card = @card.next_card)
next_card.update_attribute(:previous_id, @card.previous_id)
end

@card.list_id = target_list.id
@card.previous_id = target_list.last_card&.id
@card.save!
end

publish_event('card.updated', @card.to_event_payload)
redirect_to [current_project, @board, target_list, @card], notice: 'Task moved.'
end

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

Expand Down
11 changes: 11 additions & 0 deletions 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,6 +85,16 @@ 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?

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

def local_fields
{
'List' => list.name.parameterize(preserve_case: true, separator: '_'),
Expand Down
17 changes: 17 additions & 0 deletions app/views/cards/_actions.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
<div class="actions">
<% other_lists = @board.ordered_lists.reject { |l| l.id == @list.id } %>
Comment thread
nicolachr marked this conversation as resolved.
Outdated
<% if other_lists.any? %>
<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">
<% other_lists.each do |list| %>
<%= link_to(
move_to_list_project_board_list_card_path(current_project, @board, @list, @card, new_list_id: list.id),
method: :post,
class: 'dropdown-item'
) do %>
<%= list.name %>
<% 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, @board.lists.maximum(:updated_at)] do %>
Comment thread
nicolachr 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
5 changes: 4 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@
resources :lists, except: [:index] do
member { post :move }
resources :cards, except: [:index] do
member { post :move }
member do
post :move
post :move_to_list
end
resources :revisions, only: [:index, :show]
end
end
Expand Down
112 changes: 112 additions & 0 deletions spec/requests/cards/move_to_list_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
require 'rails_helper'

describe 'cards#move_to_list' do
before { login_to_project_as_user }

let(:board) { create(:board, node: current_project.methodology_library, project: current_project) }
let(:source_list) { create(:list, board: board) }
let(:target_list) { create(:list, board: board, previous_id: source_list.id) }

let(:card_a) { create(:card, list: source_list) }
let(:card_b) { create(:card, list: source_list, previous_id: card_a.id) }
let(:card_c) { create(:card, list: source_list, previous_id: card_b.id) }

let(:last_card_in_target) { create(:card, list: target_list) }

let(:submit) do
post move_to_list_project_board_list_card_path(current_project, board, source_list, card_b),
params: { new_list_id: target_list.id }
end

before do
card_a
card_b
card_c
last_card_in_target
end

it 'moves the card to the target list' do
submit
expect(card_b.reload.list).to eq(target_list)
end

it 'appends the card to the end of the target list' do
submit
expect(card_b.reload.previous_id).to eq(last_card_in_target.id)
end

it 'repairs the source list chain' do
submit
expect(card_c.reload.previous_id).to eq(card_a.id)
end

it 'redirects to the card in its new list with a notice' do
submit
expect(response).to redirect_to(project_board_list_card_path(current_project, board, target_list, card_b))
expect(flash[:notice]).to eq('Task moved.')
end

it 'creates an activity' do
expect { submit }.to have_enqueued_job(ActivityTrackingJob).with(
action: 'move_to_list',
project_id: current_project.id,
trackable_id: card_b.id,
trackable_type: 'Card',
user_id: @logged_in_as.id
)
end

context 'when moving the first card in the source list' do
let(:submit) do
post move_to_list_project_board_list_card_path(current_project, board, source_list, card_a),
params: { new_list_id: target_list.id }
end

it 'promotes the next card to list head' do
submit
expect(card_b.reload.previous_id).to be_nil
end
end

context 'when moving the last card in the source list' do
let(:submit) do
post move_to_list_project_board_list_card_path(current_project, board, source_list, card_c),
params: { new_list_id: target_list.id }
end

it 'leaves the remaining chain intact' do
submit
expect(card_b.reload.previous_id).to eq(card_a.id)
end
end

context 'when the target list is the same as the source list' do
let(:submit) do
post move_to_list_project_board_list_card_path(current_project, board, source_list, card_c),
params: { new_list_id: source_list.id }
end

it 'does not change the card' do
original_previous_id = card_c.previous_id
submit
expect(card_c.reload.previous_id).to eq(original_previous_id)
end

it 'redirects with an alert' do
submit
expect(response).to redirect_to(project_board_list_card_path(current_project, board, source_list, card_c))
expect(flash[:alert]).to eq('Task is already in that list.')
end
end

context 'when the target list is empty' do
let(:last_card_in_target) { nil }

before { target_list }

it 'makes the card the first item in the target list' do
submit
expect(card_b.reload.previous_id).to be_nil
end
end
end
Loading