aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-14 11:26:00 +0200
committerDanilo M. <danix@danix.xyz>2026-08-14 11:26:00 +0200
commit5694cf6feadd51a047455ae28a02d25d55f83987 (patch)
treed4f58ea4823f19b6f422f3e2065460b2ceba90c1
parent7b100de3ccf2b62ba5f00e468e632e32925f1ed3 (diff)
downloadqtmaildir-5694cf6feadd51a047455ae28a02d25d55f83987.tar.gz
qtmaildir-5694cf6feadd51a047455ae28a02d25d55f83987.zip
feat(rules): open the rules dialog on a seeded rule
The seed is a whole TagRule rather than a query string, so item 78 can reuse the same path to seed from a sender. It is a pending edit like one made with Add rule: appended, selected, Add tags focused, and written only on Save. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
-rw-r--r--src/tagrulesdialog.cpp44
-rw-r--r--src/tagrulesdialog.h18
-rw-r--r--tests/test_tagrules.cpp47
3 files changed, 109 insertions, 0 deletions
diff --git a/src/tagrulesdialog.cpp b/src/tagrulesdialog.cpp
index 226a564..ffcdf29 100644
--- a/src/tagrulesdialog.cpp
+++ b/src/tagrulesdialog.cpp
@@ -76,6 +76,11 @@ const FieldEntry kFields[] = {
} // namespace
TagRulesDialog::TagRulesDialog(QWidget *parent)
+ : TagRulesDialog(TagRule(), parent)
+{
+}
+
+TagRulesDialog::TagRulesDialog(const TagRule &seed, QWidget *parent)
: QDialog(parent)
{
setWindowTitle(tr("Tagging rules"));
@@ -347,6 +352,11 @@ TagRulesDialog::TagRulesDialog(QWidget *parent)
// Last, and after reloadList(): a header state cannot be applied before
// the columns it describes exist, and reloadList is what fills them.
restoreUiState();
+
+ // After restoreUiState, so the seeded rule's selection is not overwritten
+ // by anything the restore does to the list.
+ if (!seed.query.isEmpty())
+ seedRule(seed);
}
/// Reads the window size and the rule list's header layout back.
@@ -662,6 +672,35 @@ void TagRulesDialog::onAddRule()
m_list->setCurrentItem(m_list->topLevelItem(m_working.size() - 1));
}
+void TagRulesDialog::seedRule(const TagRule &seed)
+{
+ // Flushed first, as onAddRule does: reloadList() repaints every row from
+ // m_working, so an edit still sitting in the form would be lost.
+ applyEditsToCurrentRule();
+
+ TagRule rule = seed;
+
+ // enabled and stage are TagRule's own defaults (true, 50), matching what
+ // Add rule produces. An inconsistent default between two ways of making
+ // the same thing is worse than either default.
+
+ // Against the ids already present, not only against the file: the working
+ // list may hold unsaved rules whose ids would collide just as hard.
+ QStringList taken;
+ for (const TagRule &existing : m_working)
+ taken.append(existing.id);
+ rule.id = TagRules::uniqueId(rule.id, taken);
+
+ m_working.append(rule);
+ reloadList();
+ m_list->setCurrentItem(m_list->topLevelItem(m_working.size() - 1));
+
+ // The one field the user must supply. A rule that tags nothing fails
+ // validate(), so Save refuses it rather than writing a rule the hook
+ // would ignore.
+ m_add->setFocus();
+}
+
void TagRulesDialog::onCopyRule()
{
applyEditsToCurrentRule();
@@ -814,6 +853,11 @@ int TagRulesDialog::ruleCountForTest() const
return m_working.size();
}
+bool TagRulesDialog::currentRuleEnabledForTest() const
+{
+ return m_enabled->isChecked();
+}
+
void TagRulesDialog::setTagsForTest(const QString &tags)
{
m_add->setText(tags);
diff --git a/src/tagrulesdialog.h b/src/tagrulesdialog.h
index 8ba656b..1001745 100644
--- a/src/tagrulesdialog.h
+++ b/src/tagrulesdialog.h
@@ -52,6 +52,20 @@ class TagRulesDialog : public QDialog
public:
explicit TagRulesDialog(QWidget *parent = nullptr);
+ /// Opens with one new rule already in the working list, selected, and the
+ /// Add tags field focused. The rule is a pending edit like any other: it
+ /// reaches the file on Save and is discarded on Cancel.
+ ///
+ /// The seed is a whole TagRule rather than a query string because item 78
+ /// will seed from a sender and will want to set tags too.
+ explicit TagRulesDialog(const TagRule &seed, QWidget *parent = nullptr);
+
+ /// Appends `seed` to the working rules, selects it and focuses Add tags.
+ /// Public because the dialog is single-instance and non-modal: a second
+ /// Create tagging rule while it is open seeds the dialog already up
+ /// rather than being dropped.
+ void seedRule(const TagRule &seed);
+
/// The queries whose message counts the dialog wants, in the order its
/// rows appear. MainWindow hands these to the worker; the dialog never
/// touches the database itself, because NotmuchWorker owns the only
@@ -108,6 +122,10 @@ public:
/// 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;
+
+ /// The selected rule's enabled flag, so the seeded default is asserted
+ /// rather than assumed from TagRule's initialiser.
+ bool currentRuleEnabledForTest() const;
void addRuleForTest() { onAddRule(); }
void setTagsForTest(const QString &tags);
diff --git a/tests/test_tagrules.cpp b/tests/test_tagrules.cpp
index cc28711..37dcc19 100644
--- a/tests/test_tagrules.cpp
+++ b/tests/test_tagrules.cpp
@@ -45,6 +45,7 @@ private slots:
void aRepairedIdSurvivesASaveAndReload();
void theWarningReadsAsAWarningAndSitsBesideSave();
void aDismissedWarningComesBackWhenThereIsSomethingNewToSay();
+ void aSeededDialogOpensOnTheNewRuleWithoutWritingIt();
void aNameTypedWithSpacesIsSanitisedInTheField();
void aRuleAddedAndNamedInTheDialogSurvivesAReopen();
void unknownFieldsSurviveASave();
@@ -924,6 +925,52 @@ void TestTagRules::aRuleAddedAndNamedInTheDialogSurvivesAReopen()
QVERIFY(reloaded.warnings().isEmpty());
}
+void TestTagRules::aSeededDialogOpensOnTheNewRuleWithoutWritingIt()
+{
+ // The seeded rule is a pending edit, exactly like one made with Add rule:
+ // in the working list, selected, and NOT on disk until Save. Asserting the
+ // file is unchanged is the half that matters, since a dialog that wrote on
+ // open would tag real mail from a menu click.
+ QTemporaryDir configHome;
+ QVERIFY(configHome.isValid());
+ qputenv("XDG_CONFIG_HOME", configHome.path().toUtf8());
+ QVERIFY(QDir().mkpath(configHome.filePath(QStringLiteral("mailrules"))));
+
+ const QString stored = configHome.filePath(
+ QStringLiteral("mailrules/rules.json"));
+ QFile out(stored);
+ QVERIFY(out.open(QIODevice::WriteOnly));
+ out.write(R"({
+ "version": 1,
+ "rules": [
+ {"id": "vendor", "query": "from:vendor.example.org",
+ "add": ["vendor"], "stage": 50, "enabled": true}
+ ]
+ })");
+ out.close();
+
+ TagRule seed;
+ seed.id = QStringLiteral("weekly-digest");
+ seed.query = QStringLiteral("from:digest.example.org");
+
+ TagRulesDialog dialog(seed);
+
+ // Appended after the rules already in the file, and current.
+ QCOMPARE(dialog.ruleCountForTest(), 2);
+ QCOMPARE(dialog.nameLineForTest(), QStringLiteral("weekly-digest"));
+ QCOMPARE(dialog.queryLineForTest(),
+ QStringLiteral("from:digest.example.org"));
+
+ // Enabled, like a rule made with Add rule. A rule created disabled and
+ // then forgotten is its own silent failure.
+ QVERIFY(dialog.currentRuleEnabledForTest());
+
+ // Nothing written. Reread from disk rather than trusting the dialog.
+ TagRules onDisk;
+ onDisk.load(stored);
+ QCOMPARE(onDisk.rules().size(), 1);
+}
+
void TestTagRules::aFolderRowUsesTheDropdownAndKeepsItsSuffix()
{
// A path: without its suffix matches nothing and notmuch says nothing