diff options
| -rw-r--r-- | src/tagrulesdialog.cpp | 44 | ||||
| -rw-r--r-- | src/tagrulesdialog.h | 18 | ||||
| -rw-r--r-- | tests/test_tagrules.cpp | 47 |
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 |
