Skip to content

Commit db4d0a8

Browse files
t3dotggclaude
andauthored
fix(web): sidebar drag and drop no longer snaps back (#16291)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 9422e2e commit db4d0a8

2 files changed

Lines changed: 66 additions & 16 deletions

File tree

‎apps/web/src/components/Sidebar.logic.test.ts‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2235,6 +2235,50 @@ describe("Working shelf (beta)", () => {
22352235
expect(resolveSidebarDropVerb("active", "working")).toBeNull();
22362236
});
22372237

2238+
it("arranges the rows it can write when another row's server cannot store an order", () => {
2239+
// None of the rows has a key yet, so the drop needs keys for its
2240+
// neighbors too. "offline" sits on a server that cannot take them.
2241+
const plan = planSidebarThreadDrop({
2242+
activeKey: "a2",
2243+
activeSection: "active",
2244+
target: {
2245+
section: "active",
2246+
pinnedOrder: [],
2247+
activeOrder: ["offline", "a2", "a1"],
2248+
},
2249+
pinnedOrder: [],
2250+
pinnedKeysById: new Map(),
2251+
activeOrder: ["offline", "a1", "a2"],
2252+
activeKeysById: new Map([
2253+
["offline", null],
2254+
["a1", null],
2255+
["a2", null],
2256+
]),
2257+
activeReorderableKeys: new Set(["a1", "a2"]),
2258+
});
2259+
expect(plan.kind).toBe("move-active");
2260+
if (plan.kind !== "move-active") return;
2261+
expect(plan.assignments.map(({ id }) => id)).toEqual(["a2", "a1"]);
2262+
const [a2, a1] = plan.assignments.map(({ orderKey }) => orderKey);
2263+
expect(a2! < a1!).toBe(true);
2264+
2265+
// A keyed row on that server still sorts by its key, so it stays a bound.
2266+
const above = planSidebarThreadDrop({
2267+
activeKey: "a2",
2268+
activeSection: "active",
2269+
target: { section: "active", pinnedOrder: [], activeOrder: ["a2", "offline"] },
2270+
pinnedOrder: [],
2271+
pinnedKeysById: new Map(),
2272+
activeOrder: ["offline", "a2"],
2273+
activeKeysById: new Map([
2274+
["offline", "m"],
2275+
["a2", "t"],
2276+
]),
2277+
activeReorderableKeys: new Set(["a2"]),
2278+
});
2279+
expect(above.kind === "move-active" && above.assignments[0]!.orderKey < "m").toBe(true);
2280+
});
2281+
22382282
it("only changes lifecycle when the inbox is time-ordered", () => {
22392283
const base = {
22402284
pinnedOrder: ["p1"],

‎apps/web/src/components/Sidebar.logic.ts‎

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,24 @@ export function planSidebarThreadDrop(input: {
336336
if (input.supportsSettlement === false && (target.section === "settled" || activeSettled)) {
337337
return { kind: "none" };
338338
}
339+
// Rows whose server cannot store an order (an older server, or a machine
340+
// that is offline) are never written. Keyless ones sort outside the keyed
341+
// run, so they leave the plan; keyed ones stay as bounds. Before, one keyless row
342+
// refused every drop that needed fresh keys for its neighbors.
343+
const arrange = (
344+
order: readonly string[],
345+
keysById: ReadonlyMap<string, string | null | undefined>,
346+
writable: ReadonlySet<string> | undefined,
347+
) => {
348+
if (!writable) return planPinnedReorder({ orderedIds: order, keysById, movedId: activeKey });
349+
if (!writable.has(activeKey)) return null;
350+
const assignments = planPinnedReorder({
351+
orderedIds: order.filter((key) => writable.has(key) || keysById.get(key) != null),
352+
keysById,
353+
movedId: activeKey,
354+
});
355+
return assignments.every(({ id }) => writable.has(id)) ? assignments : null;
356+
};
339357
switch (target.section) {
340358
case "active": {
341359
// Like the settled tail: threads can enter a time-ordered inbox, but
@@ -360,14 +378,8 @@ export function planSidebarThreadDrop(input: {
360378
) {
361379
return { kind: "none" };
362380
}
363-
const assignments = planPinnedReorder({
364-
orderedIds: order,
365-
keysById: activeKeysById,
366-
movedId: activeKey,
367-
});
368-
if (activeReorderableKeys && assignments.some(({ id }) => !activeReorderableKeys.has(id))) {
369-
return { kind: "none" };
370-
}
381+
const assignments = arrange(order, activeKeysById, activeReorderableKeys);
382+
if (assignments === null) return { kind: "none" };
371383
return {
372384
kind: "move-active",
373385
order,
@@ -389,14 +401,8 @@ export function planSidebarThreadDrop(input: {
389401
) {
390402
return { kind: "none" };
391403
}
392-
const assignments = planPinnedReorder({
393-
orderedIds: order,
394-
keysById: pinnedKeysById,
395-
movedId: activeKey,
396-
});
397-
if (reorderableKeys && assignments.some(({ id }) => !reorderableKeys.has(id))) {
398-
return { kind: "none" };
399-
}
404+
const assignments = arrange(order, pinnedKeysById, reorderableKeys);
405+
if (assignments === null) return { kind: "none" };
400406
if (activeSection === "pinned") {
401407
return assignments.length === 0
402408
? { kind: "none" }

0 commit comments

Comments
 (0)