feat(search): local saved filter sets (saved searches)#123
Merged
Conversation
Persist named, per-resource filter sets locally under `saved_searches` in config, mirroring the existing `columns` column-prefs pattern end to end. - config: new `Config.saved_searches` field + framework-free `get_saved_search`/`list_saved_searches` helpers. - writer: `_save_search`/`_delete_search` round-trip persisters in tui_commands. - TUI: FilterScreen gains Ctrl+W (save current state under a name) and Ctrl+O (load a saved set); NscTuiApp gains `saved_searches_for`/`save_search`/ `delete_search` with write-through callbacks; new name-prompt + picker modals. - CLI: `--saved <name>` on list ops resolves saved params as the base layer; explicit `--filter` and typed query options override; unknown name exits 2. - keymap/help/README updated; distinct from server-side `extras saved-filters`. Closes #119 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Owner
Author
|
Claiming: fixing the 3 code-review findings (dotted-name config corruption, missing TUI delete affordance, FilterScreen BINDINGS consistency) on branch feat/saved-searches. TDD, failing tests first. |
Saved searches persist at the dotted config path
`saved_searches.<tag>.<resource>.<name>`, and the writer splits on `.`. A
name containing `.` (e.g. `prod.v2`) was written as a nested map instead of a
leaf, so config.yaml then failed `Config.model_validate` on the next run,
bricking every subsequent nsc invocation.
Add a framework-free `validate_saved_search_name` that rejects names
containing `.`, control characters, or surrounding whitespace. Enforce it at
the point of entry (FilterScreen, with a user-facing error notification) and
defensively in the `_save_search` persister before any disk write.
Also wire a delete affordance into SavedSearchPicker: a `d` key binding
dismisses with a typed `SavedSearchChoice("delete", name)` that FilterScreen
routes to the existing `delete_search` plumbing, which previously had no UI
path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Owner
Author
|
Pushed fixes (1eed396):
lint + full suite green (1189 passed, 1 skipped). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds LOCAL, named, persisted filter sets ("saved searches") per resource, mirroring the existing
columnscolumn-prefs pattern end to end.A saved search is stored as the raw
FilterState.as_params()dict (key=value), so it round-trips back through the TUI filter screen AND throughparse_filterson the CLI. Kept framework-free and reusable by both surfaces.Changes
Config.saved_searches: dict[tag][resource][name] = {filter_key: value}field; new framework-freensc/config/saved_searches.pywithget_saved_search/list_saved_searches._save_search/_delete_searchround-trip persisters intui_commands(set_path / unset_path under acquire_lock + atomic_write, pruning empty parents).FilterScreengainsCtrl+W(save current state under a typed name) andCtrl+O(load a saved set into the current FilterState + chips); newSavedSearchNamePrompt/SavedSearchPickermodals.NscTuiAppgainssaved_searches_for/save_search/delete_searchupdating the in-memory map and calling write-through callbacks.tag/resourcethreaded intoFilterScreenfromListScreen.--saved <name>global read flag on list ops. Resolves againstconfig.saved_searches[tag][resource][name]; saved params are the base layer, explicit--filterand typed query options override them; unknown name →typer.BadParameter, exit 2.extras saved-filters.Precedence
Saved params merge UNDER explicit overrides: a typed query option or a
--filterfor the same key wins over the saved value.Notes
/api/extras/saved-filters/(different value format, per-user/instance, not available offline). Documented in--savedhelp text and README.columns, a config written by a newer nsc withsaved_searcheswill not load on an older nsc (extra='forbid'). Consistent with existing behavior.{name}_id=<id>; loading restores the id but not the human-readable FK label (chips show the id). Acceptable for v1.Tests
TDD (red-first). New/updated tests:
tests/config/test_models.py,tests/config/test_saved_searches.pytests/cli/test_tui_command.py(persist/delete),tests/cli/test_registration.py(resolve, override precedence, unknown-name exit 2)tests/tui/test_app.py,tests/tui/test_filter_screen.py(save callback, blank-name no-op, load repopulates),tests/tui/test_keymap.pyjust lint(ruff + mypy --strict) andjust test(1172 passed, 1 skipped) both green.Closes #119
🤖 Generated with Claude Code