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:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { newRule, rulesToSieve, sieveToRules, testToSieve, sieveString, upsertRule } from "../sieve";
|
||||
import { newRule, reorderRules, rulesToSieve, sieveToRules, testToSieve, sieveString, upsertRule, type SieveRule } from "../sieve";
|
||||
|
||||
describe("sieve codec", () => {
|
||||
it("escapes strings", () => {
|
||||
@@ -37,3 +37,21 @@ describe("sieve codec", () => {
|
||||
expect(sieveToRules("")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("reordering rules", () => {
|
||||
const ids = (rs: SieveRule[]) => rs.map((r) => r.id);
|
||||
const list = ["a", "b", "c", "d"].map((id) => newRule({ id }));
|
||||
|
||||
it("drops a rule above or below the card it was dropped on", () => {
|
||||
expect(ids(reorderRules(list, "a", "c", false))).toEqual(["b", "a", "c", "d"]);
|
||||
expect(ids(reorderRules(list, "a", "c", true))).toEqual(["b", "c", "a", "d"]);
|
||||
expect(ids(reorderRules(list, "d", "a", false))).toEqual(["d", "a", "b", "c"]);
|
||||
expect(ids(reorderRules(list, "b", "d", true))).toEqual(["a", "c", "d", "b"]);
|
||||
});
|
||||
it("leaves the list alone when the drop goes nowhere", () => {
|
||||
expect(reorderRules(list, "a", "a", true)).toBe(list);
|
||||
expect(reorderRules(list, "a", "zz", true)).toBe(list);
|
||||
expect(reorderRules(list, "zz", "a", true)).toBe(list);
|
||||
expect(ids(list)).toEqual(["a", "b", "c", "d"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user