Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions apps/web/src/components/Sidebar.drag.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
restrictBelowSidebarLabel,
} from "./Sidebar.drag";
import {
planSidebarThreadDrop,
resolveSidebarDropTarget,
sidebarListItemId,
sidebarMarkerId,
Expand Down Expand Up @@ -119,6 +120,63 @@ describe("sidebar collision detection", () => {
},
);

it("prepends a drop on the pinned toggle despite the clamped card overlapping the divider", () => {
const items = [pinnedHeader, thread("p2", "pinned"), divider, thread("source", "active")];
const { rects, activeIndex } = layout(items, "source", sidebarMarkerId("pinned-divider"));
const sourceRect = rects[activeIndex]!;
const collisionRect = { ...sourceRect, top: 124, bottom: 206 };
const headerNode = {
previousElementSibling: {
querySelector: () => ({
getBoundingClientRect: () => ({ top: 68, bottom: 100, left: 0, right: 260 }),
}),
},
} as unknown as HTMLElement;
const args = {
...collisionArgs(),
active: {
id: "source",
data: { current: {} },
rect: { current: { initial: sourceRect, translated: collisionRect } },
},
collisionRect,
pointerCoordinates: { x: 130, y: 84 },
droppableRects: new Map(items.map((item, index) => [sidebarListItemId(item), rects[index]!])),
droppableContainers: items.map((item, index) => ({
id: sidebarListItemId(item),
key: sidebarListItemId(item),
disabled: false,
data: { current: {} },
node: { current: item === pinnedHeader ? headerNode : null },
rect: { current: rects[index]! },
})),
};
expect(closestCenter(args)[0]?.id).toBe(sidebarMarkerId("pinned-divider"));
const detector = createSidebarCollisionDetection(() => true, {
items,
});
const overId = String(detector(args)[0]?.id);
expect(overId).toBe(sidebarMarkerId("pinned-header"));
const plan = planSidebarThreadDrop({
activeKey: "source",
activeSection: "active",
target: resolveSidebarDropTarget(items, "source", overId, ["p1", "p2", "p3"])!,
pinnedOrder: ["p2"],
pinnedKeysById: new Map([
["p1", "gg"],
["p2", "nn"],
["p3", "tt"],
]),
reorderableKeys: new Set(["source"]),
activeOrder: ["source"],
activeKeysById: new Map(),
});
expect(plan.kind).toBe("pin");
if (plan.kind !== "pin") return;
expect(plan.orderKey! < "gg").toBe(true);
expect(plan.extraAssignments).toEqual([]);
});

it.each([
{ sourceSection: "active", pins: 0 },
{ sourceSection: "active", pins: 1 },
Expand Down
22 changes: 22 additions & 0 deletions apps/web/src/components/Sidebar.drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,28 @@ export function createSidebarCollisionDetection(
}
}
}
// The clamped card stays below the toggle, so its center can select the divider.
const headerRect = pointer
? args.droppableContainers
.find((container) => container.id === sidebarMarkerId("pinned-header"))
?.node.current?.previousElementSibling?.querySelector<HTMLElement>(
'[data-testid="sidebar-pinned-shelf-toggle"]',
)
?.getBoundingClientRect()
: undefined;
if (
pointer &&
headerRect &&
pointer.x >= headerRect.left &&
pointer.x <= headerRect.right &&
pointer.y >= headerRect.top &&
pointer.y <= headerRect.bottom
) {
const header = collisions.find(
(collision) => collision.id === sidebarMarkerId("pinned-header"),
);
if (header) collisions = [header, ...collisions.filter((collision) => collision !== header)];
}
const nearest = collisions[0];
if (!nearest || nearest.id === args.active.id) {
return collisions;
Expand Down
183 changes: 183 additions & 0 deletions apps/web/src/components/Sidebar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
deleteSelectedThreadEntries,
getFallbackThreadIdAfterDelete,
hasUnseenCompletion,
isOptimisticDropReleased,
resolveSidebarRowThreadId,
belongsInSidebarWorkingShelf,
isSidebarEnvironmentDisconnected,
Expand All @@ -23,6 +24,7 @@ import {
shouldRecedeSidebarThread,
sortInboxThreadsByReturn,
resolveSidebarDropTarget,
sidebarMarkerId,
planSidebarThreadDrop,
resolveSidebarReorderableThreadKeys,
sortPinnedThreadsForSidebar,
Expand All @@ -32,6 +34,7 @@ import {
type SidebarListItem,
type SidebarListMarker,
type SidebarSection,
type SidebarOptimisticDrop,
resolveSidebarDropVerb,
groupSidebarThreadsByProject,
resolveSidebarFolderDot,
Expand Down Expand Up @@ -1108,6 +1111,186 @@ describe("Working shelf (beta)", () => {
},
);

it.each([null, "p2"])(
"prepends a collapsed-header drop before every pin with routed pin %s",
(routedPin) => {
const visiblePins = routedPin === null ? [] : [routedPin];
const pinnedKeysById = new Map([
["p1", "gg"],
["p2", "nn"],
["p3", "tt"],
]);
const target = resolveSidebarDropTarget(
[
marker("pinned-header"),
...visiblePins.map((key) => row(key, "pinned")),
marker("pinned-divider"),
row("a1", "active"),
],
"a1",
sidebarMarkerId("pinned-header"),
["p1", "p2", "p3"],
);
expect(target).not.toBeNull();
const plan = planSidebarThreadDrop({
activeKey: "a1",
activeSection: "active",
target: target!,
pinnedOrder: visiblePins,
pinnedKeysById,
reorderableKeys: new Set(["a1", ...visiblePins]),
activeOrder: ["a1"],
activeKeysById: new Map(),
});
expect(plan.kind).toBe("pin");
if (plan.kind !== "pin") return;
expect(plan.extraAssignments).toEqual([]);
expect(plan.order).toEqual(["a1", ...visiblePins]);
const persistedKeys = new Map(pinnedKeysById);
persistedKeys.set("a1", plan.orderKey!);
expect([...persistedKeys].toSorted((a, b) => a[1].localeCompare(b[1]))).toEqual([
["a1", expect.any(String)],
...pinnedKeysById,
]);
},
);

it("preserves hidden keyless pins when dropping below the routed pin in a collapsed shelf", () => {
const pins = [
{ id: "hidden-new", createdAt: "2026-03-09T12:00:00.000Z", pinOrderKey: null },
{ id: "routed", createdAt: "2026-03-09T11:00:00.000Z", pinOrderKey: null },
{ id: "hidden-old", createdAt: "2026-03-09T10:00:00.000Z", pinOrderKey: null },
];
const dropped = {
id: "dropped",
createdAt: "2026-03-09T13:00:00.000Z",
pinOrderKey: null,
};
const target = resolveSidebarDropTarget(
[
marker("pinned-header"),
row("routed", "pinned"),
marker("pinned-divider"),
row("dropped", "active"),
],
dropped.id,
sidebarMarkerId("pinned-divider"),
sortPinnedThreadsForSidebar(pins).map((thread) => thread.id),
);
expect(target).not.toBeNull();
const plan = planSidebarThreadDrop({
activeKey: dropped.id,
activeSection: "active",
target: target!,
pinnedOrder: ["routed"],
pinnedKeysById: new Map(pins.map((thread) => [thread.id, thread.pinOrderKey])),
reorderableKeys: new Set([...pins, dropped].map((thread) => thread.id)),
activeOrder: [dropped.id],
activeKeysById: new Map(),
});
expect(plan.kind).toBe("pin");
if (plan.kind !== "pin") return;
const assignedKeys = new Map([
[dropped.id, plan.orderKey!],
...plan.extraAssignments.map(({ id, orderKey }) => [id, orderKey] as const),
]);
const persisted = [...pins, dropped].map((thread) => ({
...thread,
pinOrderKey: assignedKeys.get(thread.id) ?? thread.pinOrderKey,
}));
expect(sortPinnedThreadsForSidebar(persisted).map((thread) => thread.id)).toEqual([
"hidden-new",
"routed",
"dropped",
"hidden-old",
]);
});

it.each(["x", "h1"])(
"waits for every collapsed-header key assignment with routed thread %s",
(routedId) => {
const now = "2026-03-09T12:00:00.000Z";
const threads = ["x", "h1", "h2"].map((id) =>
makeThreadFixture({
id: ThreadId.make(id),
environmentId: localEnvironmentId,
pinnedAt: id === "x" ? null : now,
pinOrderKey: null,
}),
);
const keys = threads.map((thread) =>
scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)),
);
const visiblePins = threads.flatMap((thread, index) =>
thread.pinnedAt !== null && thread.id === routedId ? [keys[index]!] : [],
);
const keysAtDrop = new Map(keys.map((key) => [key, null]));
const target = resolveSidebarDropTarget(
[
marker("pinned-header"),
...visiblePins.map((key) => row(key, "pinned")),
marker("pinned-divider"),
row(keys[0]!, "active"),
],
keys[0]!,
sidebarMarkerId("pinned-header"),
keys.slice(1),
);
expect(target).not.toBeNull();
const plan = planSidebarThreadDrop({
activeKey: keys[0]!,
activeSection: "active",
target: target!,
pinnedOrder: visiblePins,
pinnedKeysById: keysAtDrop,
reorderableKeys: new Set(keys),
activeOrder: [keys[0]!],
activeKeysById: new Map(),
});
expect(plan.kind).toBe("pin");
if (plan.kind !== "pin") return;
expect(plan.extraAssignments.map(({ id }) => id)).toEqual(keys.slice(1));
const assignedKeys = new Map([
[keys[0]!, plan.orderKey!],
...plan.extraAssignments.map(({ id, orderKey }) => [id, orderKey] as const),
]);
const optimisticDrop: SidebarOptimisticDrop = {
key: keys[0]!,
sourceSection: "active",
section: "pinned",
occurredAt: now,
clearsSnooze: true,
order: target!.pinnedKeyOrder!,
keysAtDrop,
assignedKeys,
};
const released = (canonicalThreads: readonly Thread[]) =>
isOptimisticDropReleased({
optimisticDrop,
threads: canonicalThreads,
pinnedKeys: canonicalThreads.flatMap((thread, index) =>
thread.pinnedAt !== null ? [keys[index]!] : [],
),
activeKeys: [],
now,
});
expect(released(threads)).toBe(false);
const pinLanded = threads.map((thread, index) =>
index === 0 ? { ...thread, pinnedAt: now, pinOrderKey: plan.orderKey! } : thread,
);
expect(released(pinLanded)).toBe(false);
const partiallyLanded = pinLanded.map((thread, index) =>
index === 1 ? { ...thread, pinOrderKey: assignedKeys.get(keys[index]!)! } : thread,
);
expect(released(partiallyLanded)).toBe(false);
const allLanded = pinLanded.map((thread, index) => ({
...thread,
pinOrderKey: assignedKeys.get(keys[index]!)!,
}));
expect(released(allLanded)).toBe(true);
},
);

it("arranges the rows it can write when another row's server cannot store an order", () => {
const plan = planSidebarThreadDrop({
activeKey: "a2",
Expand Down
Loading
Loading