Keep an edited filter rule where it was

Renaming or editing a Sieve rule moved it to the bottom of the list, and
in Sieve the order is the order the rules run in, so mail started being
filed by a different rule than before. Putting it back took a click per
place moved.

Two things did it. saveAndApply always appended the rule it was given —
right for a rule created from a message, wrong for one being edited. And
the "Also apply to existing messages" tick defaulted to on wherever it
was offered, so every edit in Settings went down that path, including a
plain rename.

The rule now keeps its seat: a shared upsertRule replaces by id in place
and only appends what is genuinely new. The tick defaults to on only in
"Filter messages like this…", where applying it is the point, and the
toast no longer calls an edited rule "created".

Fixes #24
This commit is contained in:
2026-08-25 06:55:47 -07:00
parent d4d218f078
commit ee998eff46
5 changed files with 37 additions and 12 deletions
+9 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { newRule, rulesToSieve, sieveToRules, testToSieve, sieveString } from "../sieve";
import { newRule, rulesToSieve, sieveToRules, testToSieve, sieveString, upsertRule } from "../sieve";
describe("sieve codec", () => {
it("escapes strings", () => {
@@ -24,6 +24,14 @@ describe("sieve codec", () => {
expect(script).toContain("# (disabled) Big");
expect(sieveToRules(script)).toEqual(rules);
});
it("keeps an edited rule in its place and appends a new one", () => {
const rules = ["r1", "r2", "r3"].map((id) => newRule({ id, name: id }));
const renamed = { ...rules[1]!, name: "Renamed" };
expect(upsertRule(rules, renamed).map((r) => r.id)).toEqual(["r1", "r2", "r3"]);
expect(upsertRule(rules, renamed)[1]!.name).toBe("Renamed");
expect(upsertRule(rules, newRule({ id: "r4" })).map((r) => r.id)).toEqual(["r1", "r2", "r3", "r4"]);
expect(rules.map((r) => r.name)).toEqual(["r1", "r2", "r3"]);
});
it("reports hand-written scripts as raw", () => {
expect(sieveToRules('require ["fileinto"];\nif true { keep; }')).toBeNull();
expect(sieveToRules("")).toEqual([]);
+8
View File
@@ -202,6 +202,14 @@ export function newRule(partial: Partial<SieveRule> = {}): SieveRule {
};
}
/**
* Replaces a rule with the same id in place, or appends it when it is new.
* Order is evaluation order in Sieve, so an edited rule has to keep its seat.
*/
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];
}
export function describeRule(r: SieveRule): string {
const tests = r.tests
.map((t) => {