fix: Preserve tags on bulk edits (take 3) (#889)

* fix: handle tags separately from entryable_attributes in bulk updates

Tags use a join table (taggings) rather than a direct column, which means
empty tag_ids clears all tags rather than meaning "no change". This caused
bulk category-only edits to accidentally clear existing tags.

This fix:
- Removes tag_ids from entryable_attributes in Entry.bulk_update!
- Adds update_tags parameter to explicitly control tag updates
- Uses params.key?(:tag_ids) in controller to detect explicit tag changes
- Preserves existing tags when tag_ids is not provided in the request

This is a cleaner architectural solution compared to tracking "touched"
state in the frontend, as it properly acknowledges the semantic difference
between column attributes and join table associations.

https://claude.ai/code/session_014CsmTwjteP4qJs6YZqCKnY

* fix: handle tags separately in API transaction updates

Apply the same pattern to the API endpoint: tags are now handled
separately from entryable_attributes to distinguish between "not
provided" (preserve existing tags) and "explicitly set to empty"
(clear all tags).

This allows API consumers to:
- Update other fields without affecting tags (omit tag_ids)
- Clear all tags (send tag_ids: [])
- Set specific tags (send tag_ids: [id1, id2])

https://claude.ai/code/session_014CsmTwjteP4qJs6YZqCKnY

* Proposed fix

* fix: improve tag handling in bulk updates for transactions

* fix: allow bulk edit to clear/preserve tags by omitting hidden multi-select field

* PR comments

* Dumb copy/paste error

* Linter

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <jjmata@jjmata.com>
This commit is contained in:
Ang Wei Feng (Ted)
2026-02-06 21:11:46 +08:00
committed by GitHub
parent 8b89d24314
commit c77971ea0d
7 changed files with 230 additions and 24 deletions

View File

@@ -257,6 +257,75 @@ class Api::V1::TransactionsControllerTest < ActionDispatch::IntegrationTest
assert_response :unauthorized
end
test "should preserve tags when tag_ids not provided in update" do
# Set up transaction with existing tags
original_tags = [ Tag.first, Tag.second ]
@transaction.tags = original_tags
@transaction.save!
# Update only the name, without providing tag_ids
update_params = {
transaction: {
name: "Updated Name Only"
}
}
put api_v1_transaction_url(@transaction),
params: update_params,
headers: api_headers(@api_key)
assert_response :success
@transaction.reload
assert_equal "Updated Name Only", @transaction.entry.name
# Tags should be preserved since tag_ids was not in the request
assert_equal original_tags.map(&:id).sort, @transaction.tag_ids.sort
end
test "should clear tags when empty tag_ids explicitly provided in update" do
# Set up transaction with existing tags
@transaction.tags = [ Tag.first, Tag.second ]
@transaction.save!
# Explicitly provide empty tag_ids to clear tags
update_params = {
transaction: {
name: "Updated Name",
tag_ids: []
}
}
put api_v1_transaction_url(@transaction),
params: update_params,
headers: api_headers(@api_key)
assert_response :success
@transaction.reload
# Tags should be cleared since tag_ids was explicitly provided as empty
assert_empty @transaction.tags
end
test "should update tags when tag_ids explicitly provided in update" do
# Set up transaction with one tag
@transaction.tags = [ Tag.first ]
@transaction.save!
new_tags = [ Tag.second ]
update_params = {
transaction: {
tag_ids: new_tags.map(&:id)
}
}
put api_v1_transaction_url(@transaction),
params: update_params,
headers: api_headers(@api_key)
assert_response :success
@transaction.reload
assert_equal new_tags.map(&:id), @transaction.tag_ids
end
# DESTROY action tests
test "should destroy transaction" do
entry_to_delete = @account.entries.create!(