diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-13 20:16:43 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-13 20:16:43 +0200 |
| commit | c9eb6fc86c947aea0075c1e9d279695cd9f9b7ee (patch) | |
| tree | 7e5339fbb7e71d29498405f9d4b1c4188f5ab4c3 /docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | |
| parent | f389db3aad498d46c95c2a95b4280ffb541043b2 (diff) | |
| parent | 9be1b13b91188cf44a40c6786a83de034988cdbd (diff) | |
| download | qtmaildir-c9eb6fc86c947aea0075c1e9d279695cd9f9b7ee.tar.gz qtmaildir-c9eb6fc86c947aea0075c1e9d279695cd9f9b7ee.zip | |
Merge branch 'saved-queries': saved queries in a file of their own
Items 23 and 82. Saved queries move out of the [queries] INI section into
~/.config/qtmaildir/queries.json, gaining the three things the INI could not
express: an order, a pinned flag, and a per-query account scope. They can now be
created, edited, reordered, unpinned and deleted from the UI rather than only by
hand-editing a config file.
The INI could not carry order at all: QSettings reads a section through
childKeys(), which sorts alphabetically and never follows the file, so the
buttons could not be arranged. Migration reads the old section once, marks every
entry pinned so nothing moves on the first launch, and leaves the config file
byte-identical, since rewriting it with QSettings would drop the user's comments
and key order.
Sent stops being a hardcoded button and becomes an ordinary entry carrying
"generated": "sent". Its query is still composed from the accounts every time
it runs, so correcting a folder name still needs no edit here, but the row now
follows one rule instead of having one member the user could not move, rename or
remove.
Item 82 was found by hand-testing item 23 and fixed on the same branch: saving
worked and nothing else did, so a saved query could be created and never
changed. It also uncovered a defect that predated it, where rebuilding the row
with deleteLater() left the stale row answering findChild(), which was already
reachable from the save path.
Hand-tested throughout: the migration against the real config, saving, unpinning,
deleting, reordering, the read-only query field on a generated entry, and an
unpin surviving a restart.
Diffstat (limited to 'docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md')
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | 68 |
1 files changed, 68 insertions, 0 deletions
diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md index 268dbb4..5e97337 100644 --- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md +++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md @@ -146,6 +146,7 @@ taking that too literally. | 80 | A rule with many conditions squeezes the rule list to one visible row | defect | XS | **done** 2026-08-13 on `rule-builder`, unreleased. Follows item 76 | | 79 | Opening the rules dialog and saving destroys the first rule | defect | XS | **fixed on `rule-builder`** 2026-08-13, unreleased. Shipped in 0.16.0; damaged one real rule, repaired by hand | | 81 | No way to turn a saved query into a tagging rule | workflow | S | open; depends on 23, which builds the dialog, and is excluded from its spec on purpose. Writes to the shared rules file, so it spans this repo and `mailctl` | +| 82 | A saved query cannot be edited, unpinned or deleted from the UI | defect | S | **done** 2026-08-13 on `saved-queries`, unreleased. Right-click offers Edit, Pin/Unpin and Delete | Sizes are rough: XS under an hour, S a sitting, M a session. @@ -574,6 +575,73 @@ failing silently. **Size: S** on top of 23, and not meaningful before it. +## 82. A saved query cannot be edited, unpinned or deleted from the UI + +**Observed (user, 2026-08-13):** hand-testing item 23. The user saved a query, +then asked how to unpin it, and there is no answer that does not involve either +a text editor or retyping the whole query. + +**Cause:** item 23 specified saving and nothing else, and that is exactly what +shipped. `SaveQueryDialog` opens on the contents of the query BAR, not on a +stored entry, so the only route to changing one field of an existing query is to +reconstruct the whole query, name it identically, and let +`MainWindow::saveCurrentQuery()` replace it by name. There is no delete at any +price: nothing in the UI removes an entry from `queries.json`. + +This is a defect rather than a missing enhancement. An action that creates +something the UI cannot then edit or remove is incomplete, and the user hit it +within minutes of the first hand test. + +**Approach.** A context menu on a saved-query button and on each **More +queries** entry, offering Edit, Unpin (or Pin) and Delete. + +- **Edit** opens `SaveQueryDialog` prefilled from the STORED entry rather than + from the query bar. The dialog already carries every field it needs; what it + lacks is a constructor that takes a `SavedQuery`. +- **Unpin** is a one-field write and does not need the dialog at all. +- **Delete** removes the entry and rewrites the file. + +**Constraints.** + +- `saveCurrentQuery()` already merges an existing entry's `unknown` fields over + the dialog's fresh value, and every one of these paths must do the same or a + field written by a later build is dropped by an edit here. +- Renaming through Edit is a rename, not a second entry: match on the name the + dialog was OPENED with, not the one it returns, or renaming silently creates a + duplicate and leaves the original behind. +- Delete is destructive and the file is user config, so it is one of the few + places in this application that wants a confirmation. The no-confirmation rule + in CLAUDE.md is about tag mutations, which are undoable through the undo + stack; this is not on that stack and cannot be undone. +- A test must exercise every route the way item 75's did not: the dialog's + Cancel goes through `done(int)` and never sends a `QCloseEvent`. + +**Size: S**, and it should land before the saved-query work is called done. + +**Done 2026-08-13.** A context menu on each button and each menu entry, with +Edit, Move to menu / Show as a button, and Delete. Every path goes through one +`replaceSavedQuery()`, matched on the name the dialog was OPENED with, so a +rename replaces rather than duplicating, and merging the stored entry's unknown +fields in one place rather than three. + +Two things the approach above did not anticipate. A GENERATED entry has no +query to edit, so the dialog shows its composed query read-only rather than +offering a field that changes nothing, and carries `generated` and `flat` +through an edit rather than letting it decay into a plain entry holding a +snapshot. And the overwrite notice had to learn to ignore the entry being +edited: warning that "Inbox" already exists while editing Inbox is noise. + +It also exposed a defect that predated it. `rebuildSavedQueryRow()` called +`deleteLater()` on the old row, which defers destruction to the event loop, so +the stale row went on answering `findChild()` and every lookup after a rebuild +saw the state from before the edit. It was already reachable from the save path. +Fixed by reparenting the row out immediately. + +The unknown-fields test initially passed against the merge being deleted: it +drove UNPIN, which copies the stored entry and therefore carries `unknown` +along by itself. It now goes through the edit path with a replacement that has +none, which is what the dialog actually returns. + ## Deferred, unsized, or split out Items noted while triaging but not part of the original list. Same numbering |
