diff --git a/cmd/root.go b/cmd/root.go index 95f1471..7424fac 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -26,6 +26,7 @@ func Execute() error { isFlag := strings.HasPrefix(first, "-") && !strings.Contains(first, " ") if first != "help" && first != "completion" && !isFlag && !isSubcommand(first) { + // "--" stops cobra from parsing glyph prefixes like "-" as flags rootCmd.SetArgs(append([]string{"add", "--"}, os.Args[1:]...)) } } diff --git a/internal/api/api_test.go b/internal/api/api_test.go index 6b720e0..bcb9b8e 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -235,16 +235,25 @@ func TestDeleteEntity_SoftThenHard(t *testing.T) { // Soft delete req, _ := http.NewRequest("DELETE", srv.URL+"/api/entities/"+created.ID, nil) resp, _ := http.DefaultClient.Do(req) + var delResp DeleteResponse + json.NewDecoder(resp.Body).Decode(&delResp) resp.Body.Close() - if resp.StatusCode != http.StatusNoContent { - t.Fatalf("soft delete: expected 204, got %d", resp.StatusCode) + if resp.StatusCode != http.StatusOK { + t.Fatalf("soft delete: expected 200, got %d", resp.StatusCode) + } + if delResp.Result != "soft" { + t.Fatalf("soft delete: expected result 'soft', got %q", delResp.Result) } // Hard delete resp, _ = http.DefaultClient.Do(req) + json.NewDecoder(resp.Body).Decode(&delResp) resp.Body.Close() - if resp.StatusCode != http.StatusNoContent { - t.Fatalf("hard delete: expected 204, got %d", resp.StatusCode) + if resp.StatusCode != http.StatusOK { + t.Fatalf("hard delete: expected 200, got %d", resp.StatusCode) + } + if delResp.Result != "hard" { + t.Fatalf("hard delete: expected result 'hard', got %q", delResp.Result) } // Gone diff --git a/internal/api/entities.go b/internal/api/entities.go index 19d0d41..0a93c62 100644 --- a/internal/api/entities.go +++ b/internal/api/entities.go @@ -214,10 +214,14 @@ func updateEntity(store *db.Store) http.HandlerFunc { } } +type DeleteResponse struct { + Result string `json:"result"` +} + func deleteEntity(store *db.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") - _, err := store.SoftDelete(id) + result, err := store.SoftDelete(id) if err != nil { if err == db.ErrNotFound { writeError(w, http.StatusNotFound, "not_found", "no entity with id "+id) @@ -226,7 +230,11 @@ func deleteEntity(store *db.Store) http.HandlerFunc { writeError(w, http.StatusInternalServerError, "internal", err.Error()) return } - w.WriteHeader(http.StatusNoContent) + label := "soft" + if result == db.DeletedHard { + label = "hard" + } + writeJSON(w, http.StatusOK, DeleteResponse{Result: label}) } } diff --git a/internal/api/router.go b/internal/api/router.go index f06b503..035d861 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -3,6 +3,7 @@ package api import ( "io/fs" "net/http" + "path" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" @@ -45,8 +46,8 @@ func spaHandler(fsys fs.FS) http.HandlerFunc { indexHTML, _ := fs.ReadFile(fsys, "index.html") return func(w http.ResponseWriter, r *http.Request) { - path := r.URL.Path - if path == "/" || path == "/cards" { + p := r.URL.Path + if p == "/" || path.Ext(p) == "" { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write(indexHTML) return diff --git a/internal/db/entities.go b/internal/db/entities.go index 152d30f..34e453f 100644 --- a/internal/db/entities.go +++ b/internal/db/entities.go @@ -269,12 +269,8 @@ func (s *Store) List(params ListParams) ([]*Entity, error) { entities = append(entities, e) } - for _, e := range entities { - tags, err := s.loadTags(e.ID) - if err != nil { - return nil, err - } - e.Tags = tags + if err := s.batchLoadTags(entities); err != nil { + return nil, err } return entities, nil @@ -452,6 +448,44 @@ func (s *Store) Resolve(prefix string) (string, error) { // helpers +func (s *Store) batchLoadTags(entities []*Entity) error { + if len(entities) == 0 { + return nil + } + + idMap := make(map[string]*Entity, len(entities)) + placeholders := make([]string, len(entities)) + args := make([]any, len(entities)) + for i, e := range entities { + e.Tags = []string{} + idMap[e.ID] = e + placeholders[i] = "?" + args[i] = e.ID + } + + query := fmt.Sprintf( + "SELECT entity_id, tag FROM entity_tags WHERE entity_id IN (%s) ORDER BY entity_id, tag", + strings.Join(placeholders, ","), + ) + + rows, err := s.db.Query(query, args...) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var entityID, tag string + if err := rows.Scan(&entityID, &tag); err != nil { + return err + } + if e, ok := idMap[entityID]; ok { + e.Tags = append(e.Tags, tag) + } + } + return rows.Err() +} + func (s *Store) loadTags(entityID string) ([]string, error) { rows, err := s.db.Query("SELECT tag FROM entity_tags WHERE entity_id = ? ORDER BY tag", entityID) if err != nil { diff --git a/internal/display/glyph.go b/internal/display/glyph.go index 422046a..02604d4 100644 --- a/internal/display/glyph.go +++ b/internal/display/glyph.go @@ -13,7 +13,7 @@ var cardGlyphMap = map[db.CardType]string{ db.CardTemplate: "◈", db.CardChecklist: "☐", db.CardDecision: "⚖", - db.CardLink: "🔗", + db.CardLink: "↗", } func DisplayGlyph(glyph db.Glyph, cardType *db.CardType) string { diff --git a/web/app.js b/web/app.js index 80ee598..cc08fbb 100644 --- a/web/app.js +++ b/web/app.js @@ -4,7 +4,7 @@ const GLYPHS = { note: '◦', todo: '▸', event: '◇', snippet: '◆', template: '◈', checklist: '☐', - decision: '⚖', link: '🔗', + decision: '⚖', link: '↗', }; const GLYPH_CLASSES = { @@ -278,11 +278,14 @@ ${shortId} ${e.time_anchor ? `@${e.time_anchor}` : ''} -