summaryrefslogtreecommitdiffstats
path: root/src/tagrulesdialog.h
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-14 11:01:58 +0200
committerDanilo M. <danix@danix.xyz>2026-08-14 11:01:58 +0200
commitb08923df88de7ba03135234aaf3c602f23e49e03 (patch)
tree5cc74979efc3470e948bcdfb1b7343e880254c07 /src/tagrulesdialog.h
parent27838b47bae379578f458ebaefa192b422b55e24 (diff)
downloadqtmaildir-b08923df88de7ba03135234aaf3c602f23e49e03.tar.gz
qtmaildir-b08923df88de7ba03135234aaf3c602f23e49e03.zip
fix(rules): stop a rule with a spaced name from vanishing on save
A rule named "justeat orders" in the field labelled Name was written to rules.json correctly and then dropped by every reader, because load() required ^[a-z0-9][a-z0-9-]*$ and the save path validated nothing. The rule stayed in the file, invisible in the dialog, never applied by the post-new hook, and the next save from the dialog would have deleted it outright. The asymmetry was the defect, not the pattern. TagRules::validate() is now the single predicate: the dialog refuses to save against it, and load() uses it to repair rather than drop, so a rule that fails is visible and fixable instead of silently discarded. - The typed name is sanitised into an id when the field is committed, so the field shows what will reach the file. uniqueId() suffixes a collision, since sanitising is many-to-one and can manufacture the duplicate that load() then drops. - An already-legal id is never rewritten, including one like "a---b" that sanitising would otherwise collapse. Rewriting valid ids would churn a file mailctl also reads. - A bad id loads repaired, with the warning kept: what is on disk is not what the hook runs until the file is saved back. Deliberately not mirrored into mailrules.py. The hook tags real mail unattended every ten minutes, where silently renaming an id is worse than dropping the rule; the file converges as soon as the dialog saves. No format change, so no version bump and no two-repo commitment. The load warning was not missing: it had been showing "1 rule could not be read and was skipped" on every open, in the same font and colour as the intro prose two lines above it, and read as more explanation. It is now a red banner beside Save, with an icon and a dismiss button, and it says the rules need attention rather than that they were skipped, which is no longer true. Dismissal is per-appearance only; a persistent one would re-hide the problem that went unnoticed for a session. Both new dialog tests were confirmed to fail with the sanitiser reverted, and the banner's styling, position and dismissal each fail under mutation. 20 of 20 suites green, 34 tests in test_tagrules. Closes item 83. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src/tagrulesdialog.h')
-rw-r--r--src/tagrulesdialog.h29
1 files changed, 29 insertions, 0 deletions
diff --git a/src/tagrulesdialog.h b/src/tagrulesdialog.h
index e8979fe..8ba656b 100644
--- a/src/tagrulesdialog.h
+++ b/src/tagrulesdialog.h
@@ -89,6 +89,28 @@ public:
void setTextModeForTest(bool on);
QString warningTextForTest() const;
+ /// The warning's appearance and place, asserted as widget properties. A
+ /// render probe cannot carry this: see "Rendering probes lie" in CLAUDE.md.
+ QString warningStyleForTest() const;
+ Qt::TextFormat warningTextFormatForTest() const;
+ bool warningIsBelowTheRuleListForTest() const;
+
+ /// Clicks the warning's dismiss button, so the test drives the same signal
+ /// the user's click does rather than calling setWarning() behind it.
+ void dismissWarningForTest();
+
+ /// Types a name and commits it the way leaving the field does. The commit
+ /// is the point: the sanitiser runs on editingFinished, so a test that
+ /// only calls setText() asserts against a field nothing has processed.
+ void setNameForTest(const QString &name);
+ QString nameLineForTest() const;
+
+ /// Adding and filling a rule the way the buttons do, so a test can drive
+ /// the whole journey the user takes rather than only its last step.
+ int ruleCountForTest() const;
+ void addRuleForTest() { onAddRule(); }
+ void setTagsForTest(const QString &tags);
+
/// Whether the text-mode toggle would be on screen. A toggle that hides
/// itself when switched on is a one-way trip, and asserting only on the
/// checked STATE passes against that, since the state is still readable
@@ -169,6 +191,11 @@ private:
void restoreUiState();
void saveUiState();
void showWarnings();
+
+ /// The only way the warning label is written. An empty string hides it.
+ /// One route in so the icon and the red styling cannot drift between the
+ /// load path, the save refusal and the text-mode notice.
+ void setWarning(const QString &text);
int currentIndex() const;
/// Writes one rule's summary onto its row. Shared by reloadList() and
@@ -232,6 +259,8 @@ private:
bool m_countColumnSized = false;
QTreeWidget *m_list = nullptr;
+ QWidget *m_warningBanner = nullptr;
+ QPushButton *m_warningClose = nullptr;
QLineEdit *m_id = nullptr;
QLineEdit *m_add = nullptr;
QLineEdit *m_remove = nullptr;