Skip to content

feat(lists): add rank reordering for user lists#2495

Open
kevincador wants to merge 1 commit into
mainfrom
feat/list-items-reorder
Open

feat(lists): add rank reordering for user lists#2495
kevincador wants to merge 1 commit into
mainfrom
feat/list-items-reorder

Conversation

@kevincador
Copy link
Copy Markdown
Contributor

Summary

  • RE Re-ordering List Items #1702
  • Add a Reorder action to owned custom list menus.
  • Add a drag-and-drop drawer for changing list item rank order.
  • POST custom list reorder changes to /users/{user_id}/lists/{list_id}/items/reorder.
  • Close the drawer and refresh list items after a successful reorder.

Notes

This focuses on custom Lists only. Watchlist and Favorites can use the same structure later once their reorder endpoints are available.

Design

Screenshot 2026-06-04 at 14 53 35 Screenshot 2026-06-04 at 14 53 40 Screenshot 2026-06-04 at 14 54 29

Interaction Flow

Screen.Recording.2026-06-04.at.14.56.18.mov

@trakt-bot trakt-bot Bot enabled auto-merge (rebase) June 4, 2026 12:59
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a drag-and-drop reordering feature for custom user lists, providing a more intuitive way for users to organize their content. The changes include a new drawer component, updated API communication for persisting rank updates, and necessary UI integrations within the existing list actions menu.

Highlights

  • Drag-and-Drop Reordering: Added a new drag-and-drop drawer interface that allows users to reorder items within their custom lists.
  • API Integration: Implemented a new API request to persist rank changes to the backend via the /items/reorder endpoint.
  • Component Enhancements: Updated the global Drawer component to support custom action snippets, enabling the new reorder functionality.
  • Testing and Logic: Added robust helper functions and unit tests to manage list item sorting and rank signature generation.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@deepsource-io
Copy link
Copy Markdown

deepsource-io Bot commented Jun 4, 2026

DeepSource Code Review

We reviewed changes in cf29bf6...22a9725 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Coverage  

Code Review Summary

Analyzer Status Updated (UTC) Details
JavaScript Jun 5, 2026 10:45a.m. Review ↗
Code coverage Jun 5, 2026 10:45a.m. Review ↗

Code Coverage Summary

Language Line Coverage (Overall)
Aggregate
67.2%
Javascript
67.2%

➟ Additional coverage metrics may have been reported. See full coverage report ↗


Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a drag-and-drop list reordering feature, adding a new ListReorderDrawer component, supporting API requests, Svelte utilities, and translation keys. Feedback on these changes highlights several critical issues: mapListItemToReorderableItem should explicitly handle non-media item types like 'person' to prevent runtime crashes, and optional chaining should be used on source.list.user before accessing its slug. Additionally, to align with codebase conventions, RxJS observables should not be auto-subscribed directly inside $derived blocks, and handleApply needs robust error handling to gracefully manage failed reorder requests.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +15 to +54
export function mapListItemToReorderableItem(
item: ListItem,
): ReorderableListItem {
if (item.type === 'movie' || item.type === 'show') {
return {
key: item.key,
listItemId: item.id,
rank: item.rank,
title: item.entry.title,
subtitle: mediaSubtitle(item.entry),
posterUrl: mediaPosterUrl(item.entry),
};
}

if (item.type === 'season') {
return {
key: item.key,
listItemId: item.id,
rank: item.rank,
title: item.entry.show.title,
subtitle: seasonLabel(item.entry.season.number),
posterUrl: item.entry.season.poster?.url.thumb ??
mediaPosterUrl(item.entry.show),
};
}

return {
key: item.key,
listItemId: item.id,
rank: item.rank,
title: item.entry.show.title,
subtitle: `${
episodeNumberLabel({
seasonNumber: item.entry.episode.season,
episodeNumber: item.entry.episode.number,
})
} - ${item.entry.episode.title}`,
posterUrl: mediaPosterUrl(item.entry.show),
};
}
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.

high

The mapListItemToReorderableItem function assumes that any item that is not a 'movie', 'show', or 'season' must be an 'episode'. However, Trakt custom lists can also contain other types of items, such as 'person'.

If a list contains a 'person', item.entry will represent a person (which has a name property but no show or episode properties). Accessing item.entry.show.title on a person item will throw a runtime TypeError and crash the reorder drawer.

We should explicitly check for item.type === 'episode' and provide a safe fallback for other types like 'person'.

export function mapListItemToReorderableItem(
  item: ListItem,
): ReorderableListItem {
  if (item.type === 'movie' || item.type === 'show') {
    return {
      key: item.key,
      listItemId: item.id,
      rank: item.rank,
      title: item.entry.title,
      subtitle: mediaSubtitle(item.entry),
      posterUrl: mediaPosterUrl(item.entry),
    };
  }

  if (item.type === 'season') {
    return {
      key: item.key,
      listItemId: item.id,
      rank: item.rank,
      title: item.entry.show.title,
      subtitle: seasonLabel(item.entry.season.number),
      posterUrl: item.entry.season.poster?.url.thumb ??
        mediaPosterUrl(item.entry.show),
    };
  }

  if (item.type === 'episode') {
    return {
      key: item.key,
      listItemId: item.id,
      rank: item.rank,
      title: item.entry.show.title,
      subtitle: episodeNumberLabel({
        seasonNumber: item.entry.episode.season,
        episodeNumber: item.entry.episode.number,
      }) + " - " + item.entry.episode.title,
      posterUrl: mediaPosterUrl(item.entry.show),
    };
  }

  return {
    key: item.key,
    listItemId: item.id,
    rank: item.rank,
    title: 'name' in item.entry ? item.entry.name : '',
    subtitle: undefined,
    posterUrl: undefined,
  };
}

Comment on lines +65 to +70
const rankOrderedItems = $derived(sortReorderableItems($list));
const rankSignature = $derived(itemOrderSignature(rankOrderedItems));
const orderedSignature = $derived(itemOrderSignature(orderedItems));
const isLoaded = $derived(!$isLoading && !$hasNextPage);
const hasChanges = $derived(isLoaded && orderedSignature !== rankSignature);
const canApply = $derived(hasChanges && !isApplying);
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.

medium

According to the codebase convention, we should avoid using Svelte's native store auto-subscription (e.g., $list, $isLoading, $hasNextPage) directly inside $derived blocks when dealing with RxJS Observables. This keeps the query data extraction pattern unified and prevents potential reactivity or subscription issues in Svelte 5 runes mode.

Instead, we should subscribe to the RxJS observables inside an $effect block, assign the values to local $state variables, and reference those state variables in our $derived blocks.

  const rankOrderedItems = $derived(sortReorderableItems(items));
  const rankSignature = $derived(itemOrderSignature(rankOrderedItems));
  const orderedSignature = $derived(itemOrderSignature(orderedItems));
  const isLoaded = $derived(!loading && !hasNext);
  const hasChanges = $derived(isLoaded && orderedSignature !== rankSignature);
  const canApply = $derived(hasChanges && !isApplying);
References
  1. Maintain consistency with the existing codebase convention of using RxJS pipe and map for query data extraction and loading states in Svelte components, rather than using Svelte 5's native store auto-subscription directly in $derived blocks, to keep the pattern unified across the application.

Comment on lines +404 to +407
userId: assertDefined(
source.list.user.slug,
"Expected user list to have a user slug",
),
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.

medium

If source.list.user is undefined, evaluating source.list.user.slug will throw a runtime TypeError: Cannot read properties of undefined (reading 'slug') before assertDefined can even run.

We should use optional chaining (source.list.user?.slug) to safely pass the value to assertDefined.

      userId: assertDefined(
        source.list.user?.slug,
        "Expected user list to have a user slug",
      ),

Comment on lines +420 to +437
async function handleApply() {
if (!canApply) {
return;
}

isApplying = true;

try {
const result = await requestReorder();

if (result) {
onClose();
await refreshReorderedItems();
}
} finally {
isApplying = false;
}
}
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.

medium

The handleApply function currently lacks error handling for cases where requestReorder() returns false or throws an exception (e.g., due to network failure or server error). If a failure occurs, the drawer remains open and isApplying is reset to false, but the user receives no feedback or indication of the failure.

Consider adding a try...catch block and handling the false return value to display an error message or toast notification to the user.

  async function handleApply() {
    if (!canApply) {
      return;
    }

    isApplying = true;

    try {
      const result = await requestReorder();

      if (result) {
        onClose();
        await refreshReorderedItems();
      } else {
        // TODO: Show a toast or error notification to the user
      }
    } catch (error) {
      console.error("Failed to reorder list:", error);
      // TODO: Show a toast or error notification to the user
    } finally {
      isApplying = false;
    }
  }

@kevincador kevincador requested a review from seferturan June 4, 2026 13:19
@seferturan seferturan force-pushed the feat/list-items-reorder branch from 618b4a2 to 22a9725 Compare June 5, 2026 10:45
Copy link
Copy Markdown
Contributor

@seferturan seferturan left a comment

Choose a reason for hiding this comment

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

Will do some fixups and refactors here. Biggest changes will be:

  • Needs to also work for lists with more than 100 items.
  • I'll add a confirmation on drawer close; given how easy it is to close the drawer, it could easily cause a loss of work.

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.

2 participants