| author | |
| committer | |
| log | db4d0a8e6e49d92a9efcd8d6f82bbed10e2bd748 |
| tree | 8a684d45ab1dad073df81de05a4c0d4a5d40c03d |
| parent | 9422e2ee12fd3e3bfdf6fe5c377672badf7b20ca |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>2 files changed, 66 insertions(+), 16 deletions(-)
apps/web/src/components/Sidebar.logic.test.ts+44| ... | ... | @@ -2235,6 +2235,50 @@ describe("Working shelf (beta)", () => { |
| 2235 | 2235 | expect(resolveSidebarDropVerb("active", "working")).toBeNull(); |
| 2236 | 2236 | }); |
| 2237 | 2237 | |
| 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 | ||
| 2238 | 2282 | it("only changes lifecycle when the inbox is time-ordered", () => { |
| 2239 | 2283 | const base = { |
| 2240 | 2284 | pinnedOrder: ["p1"], |
apps/web/src/components/Sidebar.logic.ts+22-16| ... | ... | @@ -336,6 +336,24 @@ export function planSidebarThreadDrop(input: { |
| 336 | 336 | if (input.supportsSettlement === false && (target.section === "settled" || activeSettled)) { |
| 337 | 337 | return { kind: "none" }; |
| 338 | 338 | } |
| 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 | }; | |
| 339 | 357 | switch (target.section) { |
| 340 | 358 | case "active": { |
| 341 | 359 | // Like the settled tail: threads can enter a time-ordered inbox, but |
| ... | ... | @@ -360,14 +378,8 @@ export function planSidebarThreadDrop(input: { |
| 360 | 378 | ) { |
| 361 | 379 | return { kind: "none" }; |
| 362 | 380 | } |
| 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" }; | |
| 371 | 383 | return { |
| 372 | 384 | kind: "move-active", |
| 373 | 385 | order, |
| ... | ... | @@ -389,14 +401,8 @@ export function planSidebarThreadDrop(input: { |
| 389 | 401 | ) { |
| 390 | 402 | return { kind: "none" }; |
| 391 | 403 | } |
| 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" }; | |
| 400 | 406 | if (activeSection === "pinned") { |
| 401 | 407 | return assignments.length === 0 |
| 402 | 408 | ? { kind: "none" } |