Skip to content

Add ability to reorder playlists on sidebar using drag and drop - #209

Open
ggtrigg wants to merge 2 commits into
jm2:mainfrom
ggtrigg:reorderable-playlists
Open

Add ability to reorder playlists on sidebar using drag and drop#209
ggtrigg wants to merge 2 commits into
jm2:mainfrom
ggtrigg:reorderable-playlists

Conversation

@ggtrigg

@ggtrigg ggtrigg commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

For your consideration.
This PR allows the playlists listed in the sidebar to be reordered using drag and drop.
A functionality I, at least, find useful.

Summary by CodeRabbit

  • New Features
    • Added drag-and-drop reordering for playlists in the sidebar.
    • Playlist order is saved persistently and restored across sessions.
    • Playlists without a custom position are placed after explicitly ordered playlists.
    • Reordering updates are processed in sequence to preserve the intended order.
  • Bug Fixes
    • Invalid or unchanged reorder operations are safely ignored.
    • Failed saves now provide an error alert without refreshing the sidebar.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds durable playlist sidebar ordering. GTK drag-and-drop emits validated reorder actions, PlaylistManager persists contiguous positions transactionally, and sidebar snapshots apply stored positions with fallback ordering. A migration creates, validates, and removes the ordering schema and revision triggers.

Changes

Playlist sidebar ordering

Layer / File(s) Summary
Ordering schema and migration
src/db/migration/m20260807_000019_playlist_sidebar_order.rs, src/db/migration/mod.rs
Adds the ordering table, revision triggers, strict schema revalidation, transactional installation and downgrade, and migration registration. Tests cover constraints, cascades, revision changes, and rollback behavior.
Order persistence and sidebar projection
src/local/playlist_manager.rs, src/local/playlist_sidebar.rs
Validates exact playlist permutations, stores contiguous positions transactionally, reads persisted order, and orders positioned playlists before fallback rows.
Drag-and-drop reorder flow
src/ui/sidebar.rs, src/ui/playlist_actions.rs
Adds playlist drag-and-drop handling, PlaylistAction::Reorder, asynchronous persistence, refresh handling, failure alerts, and move validation tests.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • jm2/tributary#206: Both changes use SeaORM raw database APIs in playlist sidebar code.

Suggested reviewers: jm2

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: drag-and-drop reordering of playlists in the sidebar.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codacy-production

codacy-production Bot commented Aug 7, 2026

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 4 medium · 2 minor

Alerts:
⚠ 6 issues (≤ 0 issues of at least minor severity)

Results:
6 new issues

Category Results
Complexity 4 medium
2 minor

View in Codacy

🟢 Metrics 147 complexity · 22 duplication

Metric Results
Complexity 147
Duplication 22

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/db/migration/m20260807_000019_playlist_sidebar_order.rs (1)

176-201: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the redundant named arguments.

TABLE and when are already in scope, so the inline captures resolve without the explicit TABLE = TABLE and when = when arguments. Removing them shortens the call and avoids the impression that TABLE is a local binding.

♻️ Proposed cleanup
         name = trigger.name,
         operation = trigger.operation,
-        TABLE = TABLE,
-        when = when,
         revision_table = REVISION_TABLE,
         singleton = REVISION_SINGLETON,
         max_revision = i64::MAX,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/db/migration/m20260807_000019_playlist_sidebar_order.rs` around lines 176
- 201, Remove the redundant TABLE = TABLE and when = when named arguments from
the format! call constructing the trigger in the playlist sidebar migration;
rely on the existing in-scope inline captures while preserving all other
arguments and generated SQL.
src/local/playlist_manager.rs (1)

865-879: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider one multi-row INSERT instead of a loop.

The loop runs one statement per playlist. Each statement is a separate round trip and fires the revision trigger once. A single INSERT ... VALUES (?,?),(?,?),... reduces the round trips to one. Sidebar playlist counts are normally small, so this is a throughput improvement rather than a correctness fix. If you keep the loop, the current behavior stays correct because the whole sequence runs in one transaction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/local/playlist_manager.rs` around lines 865 - 879, In the sidebar
ordering update around the transaction’s DELETE and playlist insertion loop,
replace the per-playlist INSERT statements with one multi-row INSERT containing
all ordered_ids values, while preserving the existing i64 position conversion
and empty-list behavior. Keep the operation within the same transaction and
retain the existing overflow error.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/ui/playlist_actions.rs`:
- Around line 487-505: Serialize playlist reorder persistence so action-receipt
order is preserved through PlaylistManager::set_sidebar_order; prevent a later
reorder from being overtaken by an earlier database task. Update the reorder
dispatch around rt_handle.spawn to queue writes or enforce a monotonic request
sequence, while retaining existing success and error outcomes. Add a test that
delays the first write and verifies the second reorder is the final stored
order.

---

Nitpick comments:
In `@src/db/migration/m20260807_000019_playlist_sidebar_order.rs`:
- Around line 176-201: Remove the redundant TABLE = TABLE and when = when named
arguments from the format! call constructing the trigger in the playlist sidebar
migration; rely on the existing in-scope inline captures while preserving all
other arguments and generated SQL.

In `@src/local/playlist_manager.rs`:
- Around line 865-879: In the sidebar ordering update around the transaction’s
DELETE and playlist insertion loop, replace the per-playlist INSERT statements
with one multi-row INSERT containing all ordered_ids values, while preserving
the existing i64 position conversion and empty-list behavior. Keep the operation
within the same transaction and retain the existing overflow error.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cf8db42a-59ae-4c1e-980c-41a72266def5

📥 Commits

Reviewing files that changed from the base of the PR and between 1259651 and d15e95e.

📒 Files selected for processing (6)
  • src/db/migration/m20260807_000019_playlist_sidebar_order.rs
  • src/db/migration/mod.rs
  • src/local/playlist_manager.rs
  • src/local/playlist_sidebar.rs
  • src/ui/playlist_actions.rs
  • src/ui/sidebar.rs

Comment thread src/ui/playlist_actions.rs Outdated

@codacy-production codacy-production Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull Request Overview

This PR implements drag-and-drop playlist reordering with persistent storage, but it is currently not up to standards according to Codacy analysis. The main concerns involve high cyclomatic complexity in the migration logic and the PlaylistManager, along with a logic gap in how the UI handles no-op reorder operations.

While the backend supports short-circuiting identical permutations, the UI does not consistently prevent dispatching these redundant updates. This can lead to unnecessary database transactions and potential error dialogs being shown to the user when a successful no-op occurs. These issues should be addressed to ensure the efficiency and stability of the reordering feature.

About this PR

  • The implementation of the 'ignore no-op' requirement is incomplete. Redundant database writes and potential false-positive error dialogs occur because the UI does not verify if a permutation has actually changed before dispatching the reorder action.

Test suggestions

  • Database migration correctly installs the sidebar order table and the three triggers for revision bumping.
  • PlaylistManager rejects invalid reorder requests (missing IDs, duplicates) and rolls back changes.
  • PlaylistManager short-circuits no-op reorder requests to avoid unnecessary database operations and revision increments.
  • Sidebar SQL query correctly implements the sort priority: explicit positions first, followed by unordered items by creation date.
  • UI logic correctly calculates the new ID list permutation when dragging a playlist before or after a target row.
  • UI logic correctly extracts only playlist IDs from the sidebar store, ignoring headers or other source types.

TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback

Ok(())
}

async fn validate_triggers(manager: &SchemaManager<'_>) -> Result<(), DbErr> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 MEDIUM RISK

Suggestion: The function exceeds complexity and length limits because it combines individual trigger validation with global table-state verification. Splitting these into two separate functions will make the migration easier to debug.

See Issue in Codacy
See Issue in Codacy

/// participate: sidebar order is presentation state, never a content edit.
/// Rows are stored as contiguous positions; a playlist without an order
/// row keeps the historical `created_at` fallback ordering.
pub async fn set_sidebar_order(&self, ordered_ids: &[String]) -> Result<(), DbErr> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 MEDIUM RISK

Suggestion: The method logic should be split into validation and persistence phases. Moving the permutation check and the insertion loop into private helper methods will improve readability and maintainability.

See Issue in Codacy

Comment thread src/ui/sidebar.rs
Comment on lines +60 to +61
Some(ids)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ LOW RISK

Suggestion: Avoid dispatching a database update if the drag-and-drop operation doesn't actually change the permutation. Adding an equality check here prevents redundant transactions.

Comment thread src/local/playlist_sidebar.rs Outdated
execute(
db,
format!(
"INSERT INTO playlist_sidebar_order (playlist_id, position) VALUES ({},{})",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ LOW RISK

Nitpick: Add a space after the comma in the VALUES clause for consistency with standard SQL formatting used elsewhere in the project.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/ui/playlist_actions.rs (1)

1216-1222: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a paused clock for the delay.

The test sleeps 200 ms of real time. #[tokio::test(start_paused = true)] makes tokio::time::sleep auto-advance, so the ordering assertion stays valid and the test finishes immediately.

♻️ Proposed change
-    #[tokio::test]
+    #[tokio::test(start_paused = true)]
     async fn reorder_writes_commit_in_receipt_order_when_first_is_slow() {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/playlist_actions.rs` around lines 1216 - 1222, Update the reorder
worker test around the slow_first delay to run with Tokio’s paused clock by
enabling start_paused on the tokio test, while retaining tokio::time::sleep for
the 200 ms delay so virtual time advances and the ordering assertion remains
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/ui/playlist_actions.rs`:
- Around line 541-553: Update the comment above the reorder_tx.try_send failure
path to state that a closed queue causes an immediate silent return during
teardown and does not reach the receiver; reserve the receiver’s failure
description for dropped result_tx cases. Add diagnostic logging when try_send
fails, while preserving the existing return behavior.

---

Nitpick comments:
In `@src/ui/playlist_actions.rs`:
- Around line 1216-1222: Update the reorder worker test around the slow_first
delay to run with Tokio’s paused clock by enabling start_paused on the tokio
test, while retaining tokio::time::sleep for the 200 ms delay so virtual time
advances and the ordering assertion remains unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bbc53145-fc19-400e-8561-a51cdc8883ab

📥 Commits

Reviewing files that changed from the base of the PR and between d15e95e and e6a66ae.

📒 Files selected for processing (2)
  • src/local/playlist_sidebar.rs
  • src/ui/playlist_actions.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/local/playlist_sidebar.rs

Comment on lines +541 to +553
// Queue the write behind any earlier reorder so the final stored order
// matches the last received action. If the worker is gone (window
// teardown) the queue is closed and the dropped result channel is
// treated as a failure by the receiver below.
if reorder_tx
.try_send(SidebarReorderRequest {
ordered_ids,
result_tx,
})
.is_err()
{
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the comment: the closed-queue path never reaches the receiver.

The comment states that a closed queue is "treated as a failure by the receiver below". That is not what the code does. When try_send fails, handle_reorder returns at Line 552, before spawn_local. The receiver never runs and no alert appears. The dropped-result_tx case is the only path that reaches the receiver.

Keeping the silent return is reasonable during window teardown. Fix the comment so it describes the actual behavior, and log the drop for diagnosis.

📝 Proposed comment and logging fix
     // Queue the write behind any earlier reorder so the final stored order
-    // matches the last received action. If the worker is gone (window
-    // teardown) the queue is closed and the dropped result channel is
-    // treated as a failure by the receiver below.
+    // matches the last received action. If the worker is gone (window
+    // teardown) the queue is closed and the reorder is dropped without an
+    // alert, because the window is already going away. A worker that is
+    // dropped mid-write closes `result_tx` instead, and the receiver below
+    // reports that as a failure.
     if reorder_tx
         .try_send(SidebarReorderRequest {
             ordered_ids,
             result_tx,
         })
         .is_err()
     {
+        warn!("Sidebar reorder queue closed; dropping reorder request");
         return;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Queue the write behind any earlier reorder so the final stored order
// matches the last received action. If the worker is gone (window
// teardown) the queue is closed and the dropped result channel is
// treated as a failure by the receiver below.
if reorder_tx
.try_send(SidebarReorderRequest {
ordered_ids,
result_tx,
})
.is_err()
{
return;
}
// Queue the write behind any earlier reorder so the final stored order
// matches the last received action. If the worker is gone (window
// teardown) the queue is closed and the reorder is dropped without an
// alert, because the window is already going away. A worker that is
// dropped mid-write closes `result_tx` instead, and the receiver below
// reports that as a failure.
if reorder_tx
.try_send(SidebarReorderRequest {
ordered_ids,
result_tx,
})
.is_err()
{
warn!("Sidebar reorder queue closed; dropping reorder request");
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/playlist_actions.rs` around lines 541 - 553, Update the comment above
the reorder_tx.try_send failure path to state that a closed queue causes an
immediate silent return during teardown and does not reach the receiver; reserve
the receiver’s failure description for dropped result_tx cases. Add diagnostic
logging when try_send fails, while preserving the existing return behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant