Let a filter rule be dragged into place

Twenty-five rules and two buttons that move one place at a time meant a
rule pushed to the wrong end cost ten clicks to bring back. It can now
be dragged.

A grip on the left of each card arms the drag, so the switch, the name
and the buttons still take a plain click, and the up and down buttons
stay for the keyboard. The card being dragged fades; the one under the
pointer draws a line on the edge the rule would land on, top half or
bottom.

The guard against dropping a rule onto itself reads a ref rather than
state: dragstart and the first dragover can arrive in the same frame,
and a stale read there drew a drop line on the card being dragged. Found
by driving the real thing in a browser, and covered by a test that fires
the two events back to back.
This commit is contained in:
2026-08-25 08:51:28 -07:00
parent b4fd3d3ab4
commit 696b3713ed
5 changed files with 184 additions and 5 deletions
+14
View File
@@ -210,6 +210,20 @@ export function upsertRule(rules: SieveRule[], rule: SieveRule): SieveRule[] {
return rules.some((x) => x.id === rule.id) ? rules.map((x) => (x.id === rule.id ? rule : x)) : [...rules, rule];
}
/**
* Moves the rule `fromId` to sit either side of `toId`. `below` says which,
* decided by which half of the target card the pointer was over.
*/
export function reorderRules(rules: SieveRule[], fromId: string, toId: string, below: boolean): SieveRule[] {
if (fromId === toId) return rules;
const moved = rules.find((r) => r.id === fromId);
const rest = rules.filter((r) => r.id !== fromId);
const target = rest.findIndex((r) => r.id === toId);
if (!moved || target < 0) return rules;
const at = below ? target + 1 : target;
return [...rest.slice(0, at), moved, ...rest.slice(at)];
}
export function describeRule(r: SieveRule): string {
const tests = r.tests
.map((t) => {