diff --git a/internal/api/tags.go b/internal/api/tags.go index 79607ed..5a51d23 100644 --- a/internal/api/tags.go +++ b/internal/api/tags.go @@ -13,7 +13,8 @@ type TagResponse struct { func listTags(store *db.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - tags, err := store.ListTags() + cardsOnly := r.URL.Query().Get("cards_only") == "true" + tags, err := store.ListTags(cardsOnly) if err != nil { writeInternalError(w, err) return diff --git a/internal/db/tags.go b/internal/db/tags.go index 52b8f56..bcff174 100644 --- a/internal/db/tags.go +++ b/internal/db/tags.go @@ -5,12 +5,16 @@ type TagCount struct { Count int } -func (s *Store) ListTags() ([]TagCount, error) { +func (s *Store) ListTags(cardsOnly bool) ([]TagCount, error) { + where := "WHERE e.deleted_at IS NULL" + if cardsOnly { + where += " AND e.card_type IS NOT NULL" + } rows, err := s.db.Query(` SELECT t.tag, COUNT(*) as cnt FROM entity_tags t JOIN entities e ON t.entity_id = e.id - WHERE e.deleted_at IS NULL + ` + where + ` GROUP BY t.tag ORDER BY t.tag`) if err != nil { diff --git a/internal/db/tags_test.go b/internal/db/tags_test.go index 9cd3dfe..d28d0aa 100644 --- a/internal/db/tags_test.go +++ b/internal/db/tags_test.go @@ -4,7 +4,7 @@ import "testing" func TestListTags_Empty(t *testing.T) { s := testStore(t) - tags, err := s.ListTags() + tags, err := s.ListTags(false) if err != nil { t.Fatal(err) } @@ -19,7 +19,7 @@ func TestListTags_Counts(t *testing.T) { s.Create(&Entity{Body: "b", Glyph: GlyphNote, Tags: []string{"ops"}}) s.Create(&Entity{Body: "c", Glyph: GlyphNote, Tags: []string{"home"}}) - tags, err := s.ListTags() + tags, err := s.ListTags(false) if err != nil { t.Fatal(err) } @@ -50,7 +50,7 @@ func TestListTags_ExcludesDeleted(t *testing.T) { s.Create(&Entity{Body: "alive", Glyph: GlyphNote, Tags: []string{"here"}}) - tags, err := s.ListTags() + tags, err := s.ListTags(false) if err != nil { t.Fatal(err) } @@ -61,3 +61,37 @@ func TestListTags_ExcludesDeleted(t *testing.T) { t.Errorf("expected 'here', got %q", tags[0].Tag) } } + +func TestListTags_CardsOnly(t *testing.T) { + s := testStore(t) + s.Create(&Entity{Body: "fluid", Glyph: GlyphNote, Tags: []string{"ops", "shared"}}) + + ct := CardSnippet + s.Create(&Entity{Body: "card", Glyph: GlyphNote, Tags: []string{"ops", "code"}, CardType: &ct}) + + all, err := s.ListTags(false) + if err != nil { + t.Fatal(err) + } + if len(all) != 3 { + t.Fatalf("all tags: expected 3, got %d", len(all)) + } + + cards, err := s.ListTags(true) + if err != nil { + t.Fatal(err) + } + if len(cards) != 2 { + t.Fatalf("card tags: expected 2, got %d", len(cards)) + } + counts := map[string]int{} + for _, tc := range cards { + counts[tc.Tag] = tc.Count + } + if counts["ops"] != 1 { + t.Errorf("ops count: expected 1 (card only), got %d", counts["ops"]) + } + if counts["code"] != 1 { + t.Errorf("code count: expected 1, got %d", counts["code"]) + } +} diff --git a/web/app.js b/web/app.js index 89acec0..a20149c 100644 --- a/web/app.js +++ b/web/app.js @@ -110,8 +110,10 @@ }); return resp.json(); }, - async listTags() { - const resp = await fetch('/api/tags'); + async listTags(params = {}) { + const q = new URLSearchParams(); + if (params.cards_only) q.set('cards_only', 'true'); + const resp = await fetch('/api/tags?' + q); return resp.json(); }, }; @@ -653,9 +655,9 @@ const descSnip = e.description ? `${escHtml(e.description)}` : ''; if (e.title) { const preview = e.body ? `${escHtml(e.body)}` : ''; - label = `${escHtml(e.title)}${descSnip}${preview}`; + label = `${escHtml(e.title)}${descSnip}${preview}`; } else { - label = `${escHtml(e.body)}${descSnip}`; + label = `${escHtml(e.body)}${descSnip}`; } return `