diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index d8e4bab4187c..bf2a0c0cbd6f 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -2235,6 +2235,50 @@ describe("Working shelf (beta)", () => { expect(resolveSidebarDropVerb("active", "working")).toBeNull(); }); + it("arranges the rows it can write when another row's server cannot store an order", () => { + // None of the rows has a key yet, so the drop needs keys for its + // neighbors too. "offline" sits on a server that cannot take them. + const plan = planSidebarThreadDrop({ + activeKey: "a2", + activeSection: "active", + target: { + section: "active", + pinnedOrder: [], + activeOrder: ["offline", "a2", "a1"], + }, + pinnedOrder: [], + pinnedKeysById: new Map(), + activeOrder: ["offline", "a1", "a2"], + activeKeysById: new Map([ + ["offline", null], + ["a1", null], + ["a2", null], + ]), + activeReorderableKeys: new Set(["a1", "a2"]), + }); + expect(plan.kind).toBe("move-active"); + if (plan.kind !== "move-active") return; + expect(plan.assignments.map(({ id }) => id)).toEqual(["a2", "a1"]); + const [a2, a1] = plan.assignments.map(({ orderKey }) => orderKey); + expect(a2! < a1!).toBe(true); + + // A keyed row on that server still sorts by its key, so it stays a bound. + const above = planSidebarThreadDrop({ + activeKey: "a2", + activeSection: "active", + target: { section: "active", pinnedOrder: [], activeOrder: ["a2", "offline"] }, + pinnedOrder: [], + pinnedKeysById: new Map(), + activeOrder: ["offline", "a2"], + activeKeysById: new Map([ + ["offline", "m"], + ["a2", "t"], + ]), + activeReorderableKeys: new Set(["a2"]), + }); + expect(above.kind === "move-active" && above.assignments[0]!.orderKey < "m").toBe(true); + }); + it("only changes lifecycle when the inbox is time-ordered", () => { const base = { pinnedOrder: ["p1"], diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index 7e7010d59a42..a8743024832f 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -336,6 +336,24 @@ export function planSidebarThreadDrop(input: { if (input.supportsSettlement === false && (target.section === "settled" || activeSettled)) { return { kind: "none" }; } + // Rows whose server cannot store an order (an older server, or a machine + // that is offline) are never written. Keyless ones sort outside the keyed + // run, so they leave the plan; keyed ones stay as bounds. Before, one keyless row + // refused every drop that needed fresh keys for its neighbors. + const arrange = ( + order: readonly string[], + keysById: ReadonlyMap, + writable: ReadonlySet | undefined, + ) => { + if (!writable) return planPinnedReorder({ orderedIds: order, keysById, movedId: activeKey }); + if (!writable.has(activeKey)) return null; + const assignments = planPinnedReorder({ + orderedIds: order.filter((key) => writable.has(key) || keysById.get(key) != null), + keysById, + movedId: activeKey, + }); + return assignments.every(({ id }) => writable.has(id)) ? assignments : null; + }; switch (target.section) { case "active": { // Like the settled tail: threads can enter a time-ordered inbox, but @@ -360,14 +378,8 @@ export function planSidebarThreadDrop(input: { ) { return { kind: "none" }; } - const assignments = planPinnedReorder({ - orderedIds: order, - keysById: activeKeysById, - movedId: activeKey, - }); - if (activeReorderableKeys && assignments.some(({ id }) => !activeReorderableKeys.has(id))) { - return { kind: "none" }; - } + const assignments = arrange(order, activeKeysById, activeReorderableKeys); + if (assignments === null) return { kind: "none" }; return { kind: "move-active", order, @@ -389,14 +401,8 @@ export function planSidebarThreadDrop(input: { ) { return { kind: "none" }; } - const assignments = planPinnedReorder({ - orderedIds: order, - keysById: pinnedKeysById, - movedId: activeKey, - }); - if (reorderableKeys && assignments.some(({ id }) => !reorderableKeys.has(id))) { - return { kind: "none" }; - } + const assignments = arrange(order, pinnedKeysById, reorderableKeys); + if (assignments === null) return { kind: "none" }; if (activeSection === "pinned") { return assignments.length === 0 ? { kind: "none" }