Skip to content
Merged
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
4 changes: 4 additions & 0 deletions docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,10 @@ so old bookmarks keep working.
vertically
within a column (a manual work order) and moves them between columns to set
status; both persist as one optimistic write via the card's `rank` (D44).
On release the floating card glides into its committed slot (the shared
settle tween, `src/client/dropAnimation.ts` — PROG-118; the pre-PROG-119
fly-back that forced `dropAnimation={null}` can't recur because the drop is
committed to `columns` synchronously in `onDragEnd`).
Mouse drags activate after 4px of movement (plain clicks navigate), touch
drags after a 250ms press-and-hold (plain swipes scroll the board) — D30. When
the columns overflow (a phone, where they hit their `min-w-72` floor) the row
Expand Down
19 changes: 16 additions & 3 deletions docs/decisions/PROG-118.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,19 @@ release popped. Owner feedback asked for the board's feel. *Decisions within:*
easing off. The null was PROG-43's workaround for the tween flying to the
*stale* slot; PROG-119's synchronous store notification means the
destination row is already re-rendered when the tween measures it, so the
workaround (kept on the board, which still has its local mirror) is
unnecessary here. Verified frame-by-frame: the landed row is stationary at
its final position from frame 0 after mouseup.
workaround is unnecessary. Verified frame-by-frame: the landed row is
stationary at its final position from frame 0 after mouseup. The owner then
asked for the same feel on the **board**, so the tween lives in
`src/client/dropAnimation.ts`, shared by both surfaces — the board was
never exposed to the fly-back anyway (its `columns` mirror commits
synchronously in `onDragEnd`, PROG-59), and its "doesn't flash back"
frame-sampling e2e spec still passes with the tween on.

While wiring the board tween, its PROG-59 e2e spec turned out to be failing
for an unrelated reason: `outline-move.spec.ts` archived its focuses but left
their **actions** open, and archiving a focus does not close its actions — so
every run leaked open actions onto the board, and `isolateColumn` herded the
growing pile between columns until the drag targets slid off-screen. The spec
now cancels its actions before archiving, and the board spec's overlay-clear
assertion was retimed for the settle tween (the fly-back regression it guarded
is covered by the frame-sampling spec).
8 changes: 5 additions & 3 deletions e2e/board-reorder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,11 @@ test("dragging a card up settles in place — no fly-back to the old slot", asyn
await glideY(page, c2.cx, c2.cy + 6, c0.y - 8); // drag the bottom card above the top
await expect(overlay(page)).toHaveCount(1); // overlay present mid-drag
await page.mouse.up();
// Regression: the buggy default drop animation kept the overlay clone alive
// ~250ms flying back to the origin; it must clear well inside that.
await expect(overlay(page)).toHaveCount(0, { timeout: 120 });
// The settle tween (PROG-118, shared DROP_ANIMATION ~180ms) glides the
// overlay into the card's NEW slot, then clears. The old fly-back-to-origin
// regression is covered by the frame-sampling spec below ("doesn't flash
// back"); here just require the overlay to clear once the tween ends.
await expect(overlay(page)).toHaveCount(0, { timeout: 600 });

await expect
.poll(async () => {
Expand Down
11 changes: 10 additions & 1 deletion e2e/outline-move.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import { signInAsOwner } from "./auth";
//
// Each test creates its OWN containers/actions via the API (unique names and
// prefixes), so it is independent of the ambient dev DB and of other specs; it
// archives the containers again at the end to keep the ambient views clean.
// cancels its actions and archives the containers again at the end. The cancel
// matters: archiving a focus does NOT close its actions, and the board specs
// herd every ambient open action around with isolateColumn — leaked e2e
// actions accumulate across runs until those columns overflow the viewport and
// the board drags miss their targets (how PROG-118's leftovers broke
// board-reorder's PROG-59 spec).

const tag = () => Math.random().toString(36).slice(2, 8);
const prefix = (lead: string) =>
Expand Down Expand Up @@ -122,6 +127,8 @@ test("an action drags from one arc into another (PROG-118)", async ({ page }) =>
).arcId,
).toBe(dst.id);

for (const a of [mover, anchor])
await page.request.patch(`/api/actions/${a.id}`, { data: { status: "canceled" } });
await page.request.patch(`/api/focuses/${focus.id}`, { data: { archived: true } });
});

Expand Down Expand Up @@ -187,6 +194,8 @@ test("an action drags into another focus and is re-keyed (PROG-118)", async ({ p
await page.reload();
await handleOf(page, `${to.keyPrefix}-${moved.number}`).waitFor();

for (const a of [mover, anchor])
await page.request.patch(`/api/actions/${a.id}`, { data: { status: "canceled" } });
for (const p of [from, to])
await page.request.patch(`/api/focuses/${p.id}`, { data: { archived: true } });
await page.request.patch(`/api/workspaces/${workspace.id}`, { data: { archived: true } });
Expand Down
15 changes: 15 additions & 0 deletions src/client/dropAnimation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Settle-on-drop for every DragOverlay surface (PROG-118 polish; board +
// outline): the default drop tween glides the floating card from the release
// point into its final slot while the shadow eases off, and keeps the in-list
// source ghosted until it lands. Safe now that PROG-119 made optimistic writes
// notify synchronously — by the time the overlay measures its destination the
// row/card has ALREADY re-rendered at the drop position. (Pre-PROG-119 the
// measurement hit the stale slot and the card flew back to it, which is why
// the board and the old outline section overlays used dropAnimation={null} —
// PROG-43.)
import { defaultDropAnimationSideEffects, type DropAnimation } from "@dnd-kit/core";

export const DROP_ANIMATION: DropAnimation = {
duration: 180,
sideEffects: defaultDropAnimationSideEffects({ styles: { active: { opacity: "0.3" } } }),
};
14 changes: 7 additions & 7 deletions src/client/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Link } from "wouter";
import { ACTION_STATUSES, type ActionStatus } from "../../shared/constants";
import { rankBetween } from "../../shared/rank";
import { DROP_ANIMATION } from "../dropAnimation";
import { reorder, type ColumnMap } from "../boardOrder";
import { BOARD_FILTERS_KEY, FILTER_NONE, matchesNullableId } from "../boardFilters";
import FilterBar, { useStickyFilterUrl } from "../FilterBar";
Expand Down Expand Up @@ -416,13 +417,12 @@ export default function Home({ snapshot }: { snapshot: SnapshotPayload }) {
/>
))}
</div>
{/* dropAnimation={null}: skip the default drop tween. It animates the
overlay back to the *original* dragged node, but the reorder is
already committed to state by onDragEnd, so the tween flies the card
to its old slot before the re-render snaps it to the new one
(PROG-43). Dropping it makes the card settle in place instantly —
on-brand with the instant-UI rule. */}
<DragOverlay dropAnimation={null}>
{/* On release the overlay glides into the card's committed slot
(PROG-118 polish, shared DROP_ANIMATION): onDragEnd sets `columns`
synchronously, so the tween measures the card at its NEW position —
the old fly-back that dropAnimation={null} worked around (PROG-43)
can't happen. */}
<DragOverlay dropAnimation={DROP_ANIMATION}>
{draggingAction && (
<div data-drag-overlay>
<CardView
Expand Down
16 changes: 1 addition & 15 deletions src/client/pages/Outline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import type { CSSProperties, HTMLAttributes, ReactNode } from "react";
import { Link, useLocation, useSearch } from "wouter";
import {
closestCenter,
defaultDropAnimationSideEffects,
DndContext,
DragOverlay,
KeyboardSensor,
Expand All @@ -30,7 +29,6 @@ import {
type DragEndEvent,
type DragOverEvent,
type DragStartEvent,
type DropAnimation,
} from "@dnd-kit/core";
import {
SortableContext,
Expand All @@ -52,6 +50,7 @@ import {
updateAction,
} from "../store";
import { clearDraft, readDraft, writeDraft } from "../drafts";
import { DROP_ANIMATION } from "../dropAnimation";
import { rankForInsert, rankForReorder } from "../outlineReorder";
import { byRankThenName, containerReorderRanks } from "../containerReorder";
import { loadHideDone, loadScope, saveHideDone, saveScope } from "../outlinePrefs";
Expand Down Expand Up @@ -324,19 +323,6 @@ function actionSubtreeRows(
return rows;
}

// Settle-on-drop (PROG-118 polish): the default drop tween glides the floating
// card from the release point into the row's final slot while the shadow eases
// off, and keeps the in-list source ghosted until it lands. Safe now that
// PROG-119 made optimistic writes notify synchronously — by the time the
// overlay measures its destination the row has ALREADY re-rendered at the drop
// position. (Pre-PROG-119 this measured the stale slot and the card flew back
// to it, which is why the board and the old section overlays used
// dropAnimation={null} — PROG-43.)
const DROP_ANIMATION: DropAnimation = {
duration: 180,
sideEffects: defaultDropAnimationSideEffects({ styles: { active: { opacity: "0.3" } } }),
};

// ---------- arc promotion control ----------

function ArcMenu({ action, arcs }: { action: WireAction; arcs: WireArc[] }) {
Expand Down