From b1f6660b9f1471659ead81cf527d13d93dd57146 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Sat, 31 Oct 2026 12:08:42 +0100 Subject: feat: apply a saved rule to existing mail, confirmed and undoable The dialog's Apply reaches MainWindow, which counts the messages the rule would actually move, confirms with that count, then writes through the worker. The rule is written one tag at a time inside one undo macro, because the worker's effective set is per message: a single +a -b write would make the undo strip a tag from messages that already had it. Each undo entry is filled from its own confirmation, so it reverts exactly what moved. Refused while a sync holds the write lock, checked again after the confirmation closes. Item 212. Co-Authored-By: Claude Opus 5.5 --- src/mainwindow.h | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) (limited to 'src/mainwindow.h') diff --git a/src/mainwindow.h b/src/mainwindow.h index 45d9288..5c9e87b 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -160,6 +160,13 @@ public: /// synthesising a click on a status-bar label. Q_INVOKABLE void showPendingChanges(); + /// Item 212. Counts what `rule` would change across the whole index, + /// confirms with that count, then writes through the worker with exact + /// undo. The rule has already been saved by the dialog. Refused while a + /// sync holds the write lock, since a backfill has no ids to hold until + /// the worker resolves its query. + void applyRuleToExistingMail(const TagRule &rule); + /// Whether the undo stack still holds anything. Exposed so a test can show /// that a rejected write did not take unrelated history down with it. bool canUndo() const { return m_undoStack.canUndo(); } @@ -780,7 +787,13 @@ private slots: /// describes state lasting until the sync ends, and a message that expired /// would leave rows showing a tag the database has not got and no /// explanation of why. - void announceAction(const QString &text); + /// + /// `onDone`, when given, runs once the worker has reached the write: the + /// caller's follow-up (a refresh), not a label, so it runs whichever + /// announcement owns the label by then. Not run on the sync-held branch; + /// no caller passes one while a sync holds the lock. + void announceAction(const QString &text, + const std::function &onDone = {}); /// Reacts to a sync started outside this window, by cron or by hand. /// @@ -845,6 +858,11 @@ private slots: void onRuleCountsReady(const QVector &counts, quint64 generation); private: + /// The count for applyRuleToExistingMail() arrived: confirm, then write. + /// `counts` holds the whole rule's count first, then one per single-tag + /// operation in the order ruleOperations() lists them. + void confirmRuleApply(const QVector &counts); + void buildUi(); /// Pushes the selected account's identity into the card delegate, so the @@ -1825,6 +1843,12 @@ private: /// counter. quint64 m_ruleCountGeneration = 0; + /// The rule awaiting its change count, and the generation that count + /// will carry. Shares m_ruleCountGeneration's sequence, so a pending + /// Apply and the dialog's Count matches can never be confused. + TagRule m_pendingApplyRule; + quint64 m_pendingApplyGeneration = 0; + /// The generation of a REFRESH query, run after a sync to bring the list /// up to date without disturbing it. /// -- cgit v1.2.3