Rules: retire "All accounts" — per-account migration + scoping (#333 D1)#364
Rules: retire "All accounts" — per-account migration + scoping (#333 D1)#364CityDweller wants to merge 6 commits into
Conversation
First of two PRs for the per-account Rules Manager (spec #334). This is the data-touching half, deliberately isolated ahead of the window so the migration is an easy-to-review, easy-to-revert diff. No UI/window changes here. Migration (RuleService.LoadRules): - Each unscoped rule (null AccountId) is duplicated into one rule per NON-Graph account; Graph accounts get none (server rules replace client rules there, and an all-account client rule must not silently run on a Graph mailbox — D2). - Copies get fresh Ids (reusing the source id across N copies would break selection and delete-by-id). - Graph-only profile: an unscoped rule has no valid target and is DROPPED, with a per-rule log line. Destructive but the population is ~nil (a Microsoft-only profile carrying legacy all-account CLIENT rules), and IMAP users are untouched. Per maintainer sign-off on #334; needs a release-notes callout (text in the PR description). - Idempotent by construction: defined as "eliminate null AccountId", so the absence of any unscoped rule is the completion signal — a second run is a no-op. Runs in LoadRules before any consumer sees the list, and persists once via the atomic SaveRules, so the file is fully migrated or untouched. - Wired via an optional IAccountService on RuleService (supplied in App.xaml.cs; absent in unit tests that don't exercise migration, so existing call sites are unchanged). RulesManagerViewModel: - "All accounts" removed from AccountOptions — a new rule can no longer be created unscoped, which would reintroduce the null the migration just eliminated. - New rules default to the first account. ApplyRulesAsync's null-AccountId handling is intentionally left intact as defense in depth (migration removes null from persisted data; matching still treats a stray null as all-accounts rather than crashing). Tests: +9 migration (duplication per non-Graph account, Graph excluded, Graph-only drop, already-scoped untouched, idempotency, disk persistence, no-account-service path, mixed rules) and updated two VM tests for the retired "All accounts" option. 92 rule-related tests green; full suite 1577 passed (lone failure is the known clipboard flake, passes in isolation). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two related fixes to the per-account rules UI, both surfaced while testing #364: 1. New rules now default to the Account Manager DEFAULT account (or the sole account when none is marked default), not just the first in the list — so the Account combo lands on a real selection immediately. 2. The editor panel is disabled when no rule is selected. Its fields bind to SelectedRule.*, so with no rules (SelectedRule null) they were live-but-blank and a keyboard/screen-reader user could tab onto an empty Account combo with nothing behind it. HasSelectedRule now gates the panel's IsEnabled, dropping the dead fields from the tab order until a rule exists. Tests: +2 (default-account preference over first; HasSelectedRule false with no rules, true after New). 55 VM + XAML-parse tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ity) Follow-up to the blank-combo fix, from Tim's tab-order walkthrough: with an empty rule list, Tab still reached "Delete selected rule" and "Run on Existing Mail", both of which had nothing to act on. - Delete / Save / Test now gate on HasSelectedRule (CanExecute), so they're disabled — and dropped from the tab order — until a rule is selected. - Run on Existing Mail gates on HasRules (the list being non-empty), notified on every collection change so deleting the last rule disables it again. Also fixes a bug the previous commit would have introduced: the editor panel's IsEnabled=HasSelectedRule gate wrapped the Test/Save/CLOSE button row too, so with no rule selected the Close button would have been disabled. Restructured so only the input fields are inside the disabled region; the button row stays enabled and Close always works (Test/Save disable via their own CanExecute). Tests: +3 (Delete/Save/Test disabled until a rule is selected; Run-on-existing disabled with no rules; deleting the last rule re-disables both). 58 VM + XAML-parse tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
kellylford
left a comment
There was a problem hiding this comment.
Reviewed the migration end-to-end. Solid and well-isolated — the data-only scoping makes this easy to reason about and revert. Notes below; none are hard blockers.
Verified correct
CloneForAccountcopies every persistedMailRulefield. The only two it omits —AccountDisplay,AccessibleName— are[JsonIgnore]display-only, correctly excluded. This is the main risk in a clone-based migration and it's clean.- Fresh
Idper copy (avoids the delete-by-id / selection collision). - Idempotent by construction: keys off "any null
AccountIdremaining", persists once via the atomicSaveRules. IAccountService.LoadAccounts()andAccountModel.BackendKind/IsDefault/AccountLabelall exist as used.- The two accessibility follow-up commits are sound, and the button row is correctly kept outside the
IsEnabled=HasSelectedRuleregion so Close always works.
Relationship to main
- The non-Graph duplication + Graph-only drop design is the correct response to M365/Exchange going default-on in v0.8.35 (
b26facb). The branch is based on top of that. - Branch is behind
mainby exactly one commit — the docs-only#330 (90dda75). No code conflict; the "BEHIND" is cosmetic.
One risk worth a conscious decision (low probability, but this path deletes user data irreversibly)
The drop branch fires when targets.Count == 0. That can't distinguish a genuine Graph-only profile (intended drop) from a transient where LoadAccounts() returns empty for any reason. It only bites a profile that has rules but no/empty accounts.json — where rules are unusable anyway, and every drop is logged — so I'd call it acceptable. Flagging only because it's the sole path that removes rules.
Open items (from the PR body)
- The Graph-only-drop release-notes callout is required, not optional — it runs at startup the moment this merges (not gated). Next release file is
docs/release-notes-v0.8.36.md(already onmain; csproj still says 0.8.35). - This is PR A; the per-account window is PR B (now up as #367, stacked on this branch).
Assessment: safe to approve once the release note lands.
…review) Addresses Kelly's review of #364. 1. Guard the one data-deleting path. The Graph-only "drop" branch fired whenever there were no non-Graph accounts — which also matches a TRANSIENT empty read of accounts.json (startup ordering, a locked/corrupt file). The migration now returns early when the account list is entirely empty, so it only ever drops for a genuine Graph-only profile (accounts present, none non-Graph), never for "no account context yet". Test added. 2. Release-notes callout (required, since the migration runs at startup and isn't gated). docs/release-notes-v0.8.36.md now explains, in the existing Rules Manager section: "All accounts" is retired; existing all-account rules are copied to each standard (IMAP/SMTP) account; Microsoft-365-only profiles have them removed (recreate per account); new rules pick an account. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks — both addressed. 1. Release note (required). Added to 2. The empty-accounts drop risk. Good catch — closed it. The migration now returns early when the account list is entirely empty, so the drop path only fires for a genuine Graph-only profile (accounts present, none non-Graph), never for a transient empty read (startup ordering, a locked/corrupt The rest of your notes I took as confirmations (clone completeness, fresh ids, idempotency, the BEHIND being cosmetic). Migration tests 9/9; full suite green apart from the known clipboard flake. Stack note for merge order: #364 → #367 → (editing, in progress). #367 is now gated behind a |
v0.8.36 shipped on Jul 24 and main has since bumped to 0.8.37, so the "All accounts" retirement note was sitting in an already-released file where no user would see it. Moved it to docs/release-notes-v0.8.37.md as its own "Changed" section — the Graph-only removal path is a behaviour change that warrants a heading rather than a bullet tucked under the Rules Manager accessibility fixes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Both asks confirmed addressed — the early return on an entirely-empty account list is exactly the right narrowing, and One thing had gone stale while this sat, so I fixed it on the branch rather than bouncing it back:
Verified locally on the merged tree: Release build clean, 1617/1617 tests pass (the Approving and merging. General note for the next stacked one: put the note in the next unreleased notes file and re-check it if the PR sits, since a release can ship underneath you. |
kellylford
left a comment
There was a problem hiding this comment.
Approved. Release note is now in the right file, branch is current with main, CI green on all four checks (build, integration, CodeQL, Analyze), 1617/1617 locally on the merged tree.
|
Approved, and all four checks are green on the merged head — but it can't actually merge yet, and it's my own fault: The @CityDweller — cleanest unblock is you review and approve the two commits I added ( If you'd rather own the docs commit yourself, say so and I'll drop mine — no attachment to it. |
First of two PRs for the per-account Rules Manager (spec #334, approved). This is the data-touching half, deliberately isolated ahead of the window so the migration is an easy-to-review, easy-to-revert diff — per your call on #334. No window/UI changes here; that's PR B.
Migration —
RuleService.LoadRules()Implements D1 exactly as specced:
AccountId) is duplicated into one rule per non-Graph account; Graph accounts get none (server rules replace client rules there, and an all-account client rule must not silently run on a Graph mailbox — D2).Ids — reusing the source id across N copies would break selection and delete-by-id.AccountId", so the absence of any unscoped rule is the completion signal; a second run is a no-op. Runs inLoadRules()before any consumer sees the list, and persists once via the atomicSaveRules, so the file is fully migrated or untouched — never half-duplicated.IAccountServiceonRuleService(supplied inApp.xaml.cs; absent in unit tests that don't exercise migration, so existing call sites are unchanged).RulesManagerViewModelAccountOptions— a user can no longer create a new unscoped rule, which would reintroduce the null the migration just eliminated.ApplyRulesAsync's null-AccountIdhandling is intentionally left intact as defense in depth: the migration removes null from persisted data, but matching still treats a stray null as all-accounts rather than crashing.Per the spec, the Graph-only drop must be called out. It runs at startup the moment this merges (it's not behind the feature gate), so it needs to land in that version's notes. Suggested wording — tell me which version file to add it to, or I'll add it wherever you prefer:
Why this is safe
Tests
RuleMigrationTests): duplication per non-Graph account with fresh ids, Graph excluded, Graph-only drop, already-scoped untouched, idempotency across two loads, disk persistence, the no-account-service path, and mixed scoped/unscoped.RulesManagerViewModelTestsfor the retired "All accounts" option.OpenClipboard Failedclipboard flake, which passes in isolation.Next
PR B — the window: per-account selector, combined server+client list with "runs where" markers and group headings, the modeless conversion (your sign-off), the modeless editor, F6 ring, command palette, and
App.xaml.csDI wiring forIServerRuleService.🤖 Generated with Claude Code