feat: vi-style / search in the TUI sidebar with tag:, category: and ISO-date filters (issue #31)

This commit is contained in:
2026-07-16 21:42:19 +02:00
parent 3675a26407
commit e027364c0e
17 changed files with 1044 additions and 750 deletions

View File

@@ -524,6 +524,101 @@ defmodule BDS.TUITest do
end
end
describe "sidebar search" do
setup %{project: project} do
{:ok, tagged} =
BDS.Posts.create_post(%{
project_id: project.id,
title: "Tagged Post",
content: "x",
tags: ["elixir"],
categories: ["news"]
})
{:ok, tagged: tagged}
end
defp item_titles(state) do
for {:item, item} <- state.items, do: to_string(item[:title] || item[:name])
end
defp search(state, query) do
state |> press("/") |> type(query)
end
test "'/' opens the prompt and typing filters the list live" do
state = mount!() |> search("tagged")
assert state.search != nil
assert item_titles(state) == ["Tagged Post"]
assert screen_text(state) =~ "/tagged"
end
test "tag: filters by tag" do
state = mount!() |> search("tag:elixir")
assert item_titles(state) == ["Tagged Post"]
end
test "category: filters by category" do
state = mount!() |> search("category:news")
assert item_titles(state) == ["Tagged Post"]
end
test "an ISO date shows the posts from that date", %{post: post} do
on_ms =
DateTime.new!(~D[2026-07-04], ~T[10:00:00], "Etc/UTC") |> DateTime.to_unix(:millisecond)
BDS.Repo.update_all(
from(p in BDS.Posts.Post, where: p.id == ^post.id),
set: [updated_at: on_ms]
)
state = mount!() |> search("2026-07-04")
assert item_titles(state) == ["Hello TUI"]
end
test "tokens combine: tag plus text must both match" do
state = mount!() |> search("tag:elixir hello")
assert item_titles(state) == []
end
test "enter keeps the filter and shows it in the sidebar title" do
state = mount!() |> search("tag:elixir") |> press("enter")
assert state.search == nil
assert item_titles(state) == ["Tagged Post"]
assert screen_text(state) =~ "/tag:elixir"
end
test "esc clears the filter and restores the full list" do
state = mount!() |> search("tag:elixir") |> press("esc")
assert state.search == nil
assert "Hello TUI" in item_titles(state)
assert "Tagged Post" in item_titles(state)
end
test "the filter applies per view and works for media", %{project: project} do
media_file = Path.join(System.tmp_dir!(), "bds-tui-media-#{System.unique_integer([:positive])}.txt")
File.write!(media_file, "media body")
on_exit(fn -> File.rm(media_file) end)
{:ok, _media} =
BDS.Media.import_media(%{
project_id: project.id,
source_path: media_file,
title: "Findable Media"
})
state = mount!() |> press("2") |> search("findable") |> press("enter")
assert item_titles(state) == ["Findable Media"]
# posts view keeps its own (empty) filter
state = press(state, "1")
assert "Hello TUI" in item_titles(state)
end
end
describe "path completion" do
setup do
base = Path.join(System.tmp_dir!(), "bds-tui-tab-#{System.unique_integer([:positive])}")

View File

@@ -175,6 +175,47 @@ defmodule BDS.UI.SidebarTest do
assert Enum.map(import_view.items, & &1.title) == ["New Import", "Old Import"]
end
test "posts and media can be filtered to a single ISO date", %{
project: project,
temp_dir: temp_dir
} do
on_ms =
DateTime.new!(~D[2026-07-04], ~T[10:00:00], "Etc/UTC") |> DateTime.to_unix(:millisecond)
off_ms =
DateTime.new!(~D[2026-07-05], ~T[10:00:00], "Etc/UTC") |> DateTime.to_unix(:millisecond)
assert {:ok, on_post} = Posts.create_post(%{project_id: project.id, title: "On Date"})
assert {:ok, off_post} = Posts.create_post(%{project_id: project.id, title: "Off Date"})
update_post_sidebar_row(on_post.id, updated_at: on_ms)
update_post_sidebar_row(off_post.id, updated_at: off_ms)
view = Sidebar.view(project.id, "posts", %{year: 2026, month: 7, day: 4})
assert titles_in_section(view, "Drafts") == ["On Date"]
assert view.filters.has_active_filters
on_file = Path.join(temp_dir, "on.txt")
off_file = Path.join(temp_dir, "off.txt")
File.write!(on_file, "on")
File.write!(off_file, "off")
assert {:ok, on_media} =
Media.import_media(%{project_id: project.id, source_path: on_file, title: "Media On"})
assert {:ok, off_media} =
Media.import_media(%{
project_id: project.id,
source_path: off_file,
title: "Media Off"
})
update_media_sidebar_row(on_media.id, updated_at: on_ms)
update_media_sidebar_row(off_media.id, updated_at: off_ms)
media_view = Sidebar.view(project.id, "media", %{year: 2026, month: 7, day: 4})
assert Enum.map(media_view.items, & &1.title) == ["Media On"]
end
defp titles_in_section(view, title) do
view.sections
|> Enum.find(&(&1.title == title))