feat: select text across bullets and delete it like a text editor; Ctrl+Z in tasks; notes and lists without emoji; custom done columns complete tasks
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m33s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m33s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e2b9daba75
commit
6fb98d4002
27 files changed
+535
-64
No files matched your search
@@ -47,7 +47,7 @@ func (s *Server) handleCreateDocument(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
SpaceID string `json:"spaceId"`
|
||||
Title string `json:"title"`
|
||||
Icon string `json:"icon"`
|
||||
Icon *string `json:"icon"` // "" = no icon; missing = the default
|
||||
FolderPath string `json:"folderPath"`
|
||||
Blocks []domain.DocumentBlock `json:"blocks"`
|
||||
Tags []string `json:"tags"`
|
||||
@@ -58,7 +58,12 @@ func (s *Server) handleCreateDocument(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
doc, err := s.store.CreateDocument(userID, req.SpaceID, req.Title, req.Icon, req.FolderPath, req.Blocks, req.Tags, req.IsPinned)
|
||||
doc, err := s.store.CreateDocument(userID, req.SpaceID, req.Title, derefIcon(req.Icon), req.FolderPath, req.Blocks, req.Tags, req.IsPinned)
|
||||
if err == nil && doc != nil && req.Icon != nil && *req.Icon == "" {
|
||||
if err = s.store.ClearIcon("documents", doc.ID); err == nil {
|
||||
doc.Icon = ""
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
@@ -330,3 +335,11 @@ func (s *Server) docTitle(userID, docID string) string {
|
||||
}
|
||||
return "Unbenannte Notiz"
|
||||
}
|
||||
|
||||
// derefIcon: the icon of a create request; a missing one lets the store pick the default.
|
||||
func derefIcon(icon *string) string {
|
||||
if icon == nil {
|
||||
return ""
|
||||
}
|
||||
return *icon
|
||||
}
|
||||
@@ -39,7 +39,7 @@ func (s *Server) handleCreateList(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
var req struct {
|
||||
Title string `json:"title"`
|
||||
Icon string `json:"icon"`
|
||||
Icon *string `json:"icon"` // "" = no icon; missing = the default
|
||||
ListType string `json:"listType"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
@@ -47,7 +47,12 @@ func (s *Server) handleCreateList(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
lst, err := s.store.CreateList(userID, req.Title, req.Icon, req.ListType)
|
||||
lst, err := s.store.CreateList(userID, req.Title, derefIcon(req.Icon), req.ListType)
|
||||
if err == nil && lst != nil && req.Icon != nil && *req.Icon == "" {
|
||||
if err = s.store.ClearIcon("lists", lst.ID); err == nil {
|
||||
lst.Icon = ""
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
|
||||
@@ -28,14 +28,19 @@ func (s *Server) handleCreateTaskList(w http.ResponseWriter, r *http.Request) {
|
||||
SpaceID string `json:"spaceId"`
|
||||
TeamID string `json:"teamId"`
|
||||
Title string `json:"title"`
|
||||
Icon string `json:"icon"`
|
||||
Icon *string `json:"icon"` // "" = no icon; missing = the default
|
||||
Color string `json:"color"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||||
return
|
||||
}
|
||||
tl, err := s.store.CreateTaskList(userID, req.SpaceID, req.TeamID, req.Title, req.Icon, req.Color)
|
||||
tl, err := s.store.CreateTaskList(userID, req.SpaceID, req.TeamID, req.Title, derefIcon(req.Icon), req.Color)
|
||||
if err == nil && tl != nil && req.Icon != nil && *req.Icon == "" {
|
||||
if err = s.store.ClearIcon("task_lists", tl.ID); err == nil {
|
||||
tl.Icon = ""
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
|
||||
@@ -958,3 +958,15 @@ func (s *Store) GetCollaborators(currentUserID string) ([]domain.User, error) {
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// ClearIcon removes the icon of a note, task list or list (created on purpose without one; the create
|
||||
// functions put in a default for an empty icon).
|
||||
func (s *Store) ClearIcon(table, id string) error {
|
||||
switch table {
|
||||
case "documents", "task_lists", "lists":
|
||||
default:
|
||||
return fmt.Errorf("unknown table %q", table)
|
||||
}
|
||||
_, err := s.db.Exec(`UPDATE `+table+` SET icon = '' WHERE id = ?`, id)
|
||||
return err
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sandrodach/mindsift/internal/domain"
|
||||
)
|
||||
|
||||
func inTaskTrash(s *Store, id string) bool {
|
||||
@@ -215,3 +217,31 @@ func TestPurgeExpiredAreas(t *testing.T) {
|
||||
t.Fatal("things from before the update count from the update on")
|
||||
}
|
||||
}
|
||||
|
||||
// Moving a card into a list's own done column (any ID) completes the task, other columns reopen it.
|
||||
func TestReorderIntoCustomDoneColumn(t *testing.T) {
|
||||
s, err := NewStore(filepath.Join(t.TempDir(), "donecol.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer s.Close()
|
||||
anna, _ := s.CreateUser("anna@example.com", "hash", "Anna", "member")
|
||||
list, _ := s.CreateTaskList(anna.ID, "", "", "Projekt", "", "")
|
||||
cols := []domain.KanbanColumn{{ID: "todo", Name: "Offen"}, {ID: "col_fertig", Name: "Fertig", IsDone: true}}
|
||||
if _, err := s.UpdateTaskList(anna.ID, list.ID, nil, nil, nil, nil, &cols); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
task, _ := s.CreateThoughtWithDetails(anna.ID, "Bericht", "normal", "manual", "", list.ID, "todo", "")
|
||||
_, _ = s.UpdateThoughtTriage(anna.ID, task.ID, "keep", "task", "", "", list.ID)
|
||||
done, open := "col_fertig", "todo"
|
||||
if err := s.ReorderTasks(list.ID, []domain.ThoughtReorderItem{{ID: task.ID, SortOrder: 0, KanbanStatus: &done}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, _ := s.GetThoughtByIDUnchecked(task.ID); !got.IsCompleted {
|
||||
t.Fatal("the list's done column should complete the task")
|
||||
}
|
||||
_ = s.ReorderTasks(list.ID, []domain.ThoughtReorderItem{{ID: task.ID, SortOrder: 0, KanbanStatus: &open}})
|
||||
if got, _ := s.GetThoughtByIDUnchecked(task.ID); got.IsCompleted {
|
||||
t.Fatal("an open column reopens it")
|
||||
}
|
||||
}
|
||||
@@ -238,7 +238,7 @@ func (s *Store) ReorderTasks(taskListID string, items []domain.ThoughtReorderIte
|
||||
for _, it := range items {
|
||||
if it.KanbanStatus != nil && *it.KanbanStatus != "" {
|
||||
isCompleted := 0
|
||||
if *it.KanbanStatus == "done" {
|
||||
if isDoneColumn(tx, it.ID, *it.KanbanStatus) {
|
||||
isCompleted = 1
|
||||
var was int
|
||||
_ = tx.QueryRow(`SELECT is_completed FROM thoughts WHERE id = ?`, it.ID).Scan(&was)
|
||||
@@ -275,6 +275,24 @@ func (s *Store) ReorderTasks(taskListID string, items []domain.ThoughtReorderIte
|
||||
return nil
|
||||
}
|
||||
|
||||
// isDoneColumn: whether a Kanban column completes the task: the column of the task's list marked as done
|
||||
// (its ID may be anything), else the default ID "done".
|
||||
func isDoneColumn(q interface {
|
||||
QueryRow(query string, args ...any) *sql.Row
|
||||
}, taskID, column string) bool {
|
||||
var raw string
|
||||
_ = q.QueryRow(`SELECT COALESCE(tl.columns, '') FROM thoughts t LEFT JOIN task_lists tl ON tl.id = t.task_list_id WHERE t.id = ?`, taskID).Scan(&raw)
|
||||
var cols []domain.KanbanColumn
|
||||
if raw != "" && json.Unmarshal([]byte(raw), &cols) == nil && len(cols) > 0 {
|
||||
for _, c := range cols {
|
||||
if c.ID == column {
|
||||
return c.IsDone
|
||||
}
|
||||
}
|
||||
}
|
||||
return column == "done"
|
||||
}
|
||||
|
||||
// UpdateThoughtTriage handles swipe left (discard), swipe right (keep), restore, and conversion to task/document/list_item
|
||||
func (s *Store) UpdateThoughtTriage(userID, thoughtID, action, convertTo, targetListID, dueDate, taskListID string) (*domain.Thought, error) {
|
||||
thought, err := s.GetThoughtByID(userID, thoughtID)
|
||||
|
||||
Reference in new issue
Block a user