authorgravatar for hello@ipanasenko.meIllia Panasenko <hello@ipanasenko.me> 2026-08-20 23:23:18+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-20 17:23:18-04:00
log1afe5545b7d0397bbd6515981372072d146002ad
treeb2a778faa6441e2b3e222a86695772544c116043
parent730ce9edd9873144c1d2b01e5f1c85414c3760ad
signature Signed by PGP key B5690EEEBB952194

fix(web): fix thread jumping after reorder (#7103)


3 files changed, 37 insertions(+), 0 deletions(-)

apps/web/src/components/Sidebar.logic.test.ts+28
......@@ -1,5 +1,7 @@
11import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
2import { defaultAnimateLayoutChanges, type AnimateLayoutChanges } from "@dnd-kit/sortable";
23import {
4 animatePinnedLayoutChanges,
35 archiveSelectedThreadEntries,
46 buildBulkTitleRegenerationContextMenuItem,
57 buildMultiSelectThreadContextMenuItems,
......@@ -53,6 +55,32 @@ import {
5355
5456const localEnvironmentId = EnvironmentId.make("environment-local");
5557
58describe("animatePinnedLayoutChanges", () => {
59 const baseArgs: Parameters<AnimateLayoutChanges>[0] = {
60 active: null,
61 containerId: "pinned-threads",
62 isDragging: false,
63 isSorting: false,
64 id: "thread-a",
65 index: 1,
66 items: ["thread-b", "thread-a"],
67 newIndex: 0,
68 previousItems: ["thread-a", "thread-b"],
69 previousContainerId: "pinned-threads",
70 transition: { duration: 200, easing: "ease" },
71 wasDragging: true,
72 };
73
74 it("does not replay layout movement after the pointer is released", () => {
75 expect(defaultAnimateLayoutChanges(baseArgs)).toBe(true);
76 expect(animatePinnedLayoutChanges(baseArgs)).toBe(false);
77 });
78
79 it("keeps layout movement while the user is sorting", () => {
80 expect(animatePinnedLayoutChanges({ ...baseArgs, isSorting: true })).toBe(true);
81 });
82});
83
5684describe("shouldNavigateAfterProjectRemoval", () => {
5785 const projectThreads = [{ environmentId: "environment-local", id: "thread-1" }];
5886
apps/web/src/components/Sidebar.logic.ts+7
......@@ -1,4 +1,5 @@
11import * as React from "react";
2import { defaultAnimateLayoutChanges, type AnimateLayoutChanges } from "@dnd-kit/sortable";
23import type { ContextMenuItem } from "@t3tools/contracts";
34import type { SidebarProjectSortOrder, SidebarThreadSortOrder } from "@t3tools/contracts/settings";
45import {
......@@ -23,6 +24,12 @@ export const THREAD_JUMP_HINT_SHOW_DELAY_MS = 100;
2324// it small; cold opens still render instantly from the cached snapshot.
2425export const SIDEBAR_THREAD_PREWARM_LIMIT = 3;
2526
27// The list already reaches its destination through sortable transforms while
28// the pointer is down. dnd-kit's default also animates the committed DOM order
29// after release, replaying the same movement across every affected row.
30export const animatePinnedLayoutChanges: AnimateLayoutChanges = (args) =>
31 args.isSorting ? defaultAnimateLayoutChanges(args) : false;
32
2633type SidebarProject = {
2734 id: string;
2835 title: string;
apps/web/src/components/Sidebar.tsx+2
......@@ -121,6 +121,7 @@ import type { SidebarThreadSummary } from "../types";
121121import { cn } from "~/lib/utils";
122122import { buildThreadActionMenuItems } from "./threadActionMenu.logic";
123123import {
124 animatePinnedLayoutChanges,
124125 buildBulkTitleRegenerationContextMenuItem,
125126 formatWorkingDurationLabel,
126127 firstValidTimestampMs,
......@@ -448,6 +449,7 @@ function SortablePinnedThreadRow(props: {
448449}) {
449450 const { listeners, setNodeRef, transform, transition, isDragging } = useSortable({
450451 id: props.id,
452 animateLayoutChanges: animatePinnedLayoutChanges,
451453 });
452454 return props.children({ listeners, setNodeRef, transform, transition, isDragging });
453455}