Remove blocked column from kanban board
Test and publish / verify (push) Successful in 2m41s

This commit is contained in:
2026-08-02 12:52:08 +03:30
parent 0800487ddd
commit b0f486f026
6 changed files with 289 additions and 42 deletions
+140 -4
View File
@@ -105,6 +105,8 @@ type BoardTask struct {
ArchivedAt string
Assignees []User
Tags []BoardTag
Todos []BoardTodo
TodoDone int
}
type BoardTag struct {
@@ -113,6 +115,13 @@ type BoardTag struct {
Color string
}
type BoardTodo struct {
ID int64
TaskID int64
Body string
Completed bool
}
type Store struct{ db *sql.DB }
func OpenStore(path string) (*Store, error) {
@@ -230,7 +239,7 @@ CREATE TABLE IF NOT EXISTS board_tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'backlog' CHECK(status IN ('backlog','in_progress','blocked','done')),
status TEXT NOT NULL DEFAULT 'backlog' CHECK(status IN ('backlog','todo','in_progress','blocked','done')),
importance TEXT NOT NULL DEFAULT 'normal' CHECK(importance IN ('low','normal','high','urgent')),
assignee_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
creator_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
@@ -245,6 +254,14 @@ CREATE TABLE IF NOT EXISTS board_task_assignees (
PRIMARY KEY(task_id,user_id)
);
CREATE INDEX IF NOT EXISTS idx_board_task_assignees_user ON board_task_assignees(user_id,task_id);
CREATE TABLE IF NOT EXISTS board_task_todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL REFERENCES board_tasks(id) ON DELETE CASCADE,
body TEXT NOT NULL,
completed INTEGER NOT NULL DEFAULT 0 CHECK(completed IN (0,1)),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_board_task_todos_task ON board_task_todos(task_id,completed,created_at);
CREATE TABLE IF NOT EXISTS board_tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE COLLATE NOCASE,
@@ -270,6 +287,9 @@ CREATE INDEX IF NOT EXISTS idx_board_task_tags_tag ON board_task_tags(tag_id,tas
if err := s.ensureColumn("board_tasks", "importance", "importance TEXT NOT NULL DEFAULT 'normal' CHECK(importance IN ('low','normal','high','urgent'))"); err != nil {
return err
}
if err := s.ensureBoardTaskTodoStatus(); err != nil {
return err
}
if _, err := s.db.Exec(`INSERT OR IGNORE INTO board_task_assignees(task_id,user_id)
SELECT id,assignee_id FROM board_tasks WHERE assignee_id IS NOT NULL`); err != nil {
return err
@@ -297,6 +317,57 @@ CREATE INDEX IF NOT EXISTS idx_board_task_tags_tag ON board_task_tags(tag_id,tas
return nil
}
func (s *Store) ensureBoardTaskTodoStatus() error {
var tableSQL string
err := s.db.QueryRow(`SELECT COALESCE(sql,'') FROM sqlite_master WHERE type='table' AND name='board_tasks'`).Scan(&tableSQL)
if err != nil {
return err
}
if strings.Contains(strings.ToLower(tableSQL), "'todo'") {
return nil
}
if _, err := s.db.Exec(`PRAGMA foreign_keys=OFF`); err != nil {
return err
}
restoreForeignKeys := func() { _, _ = s.db.Exec(`PRAGMA foreign_keys=ON`) }
if _, err := s.db.Exec(`CREATE TABLE board_tasks_migrated (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'backlog' CHECK(status IN ('backlog','todo','in_progress','blocked','done')),
importance TEXT NOT NULL DEFAULT 'normal' CHECK(importance IN ('low','normal','high','urgent')),
assignee_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
creator_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
due_date TEXT,
archived_at DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)`); err != nil {
restoreForeignKeys()
return err
}
if _, err := s.db.Exec(`INSERT INTO board_tasks_migrated(id,title,description,status,importance,assignee_id,creator_id,due_date,archived_at,created_at)
SELECT id,title,description,status,importance,assignee_id,creator_id,due_date,archived_at,created_at FROM board_tasks`); err != nil {
_, _ = s.db.Exec(`DROP TABLE board_tasks_migrated`)
restoreForeignKeys()
return err
}
if _, err := s.db.Exec(`DROP TABLE board_tasks`); err != nil {
_, _ = s.db.Exec(`DROP TABLE board_tasks_migrated`)
restoreForeignKeys()
return err
}
if _, err := s.db.Exec(`ALTER TABLE board_tasks_migrated RENAME TO board_tasks`); err != nil {
restoreForeignKeys()
return err
}
if _, err := s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_board_tasks_status ON board_tasks(status,created_at)`); err != nil {
restoreForeignKeys()
return err
}
restoreForeignKeys()
return nil
}
func hashPassword(password string) (string, error) {
salt := make([]byte, 16)
if _, err := rand.Read(salt); err != nil {
@@ -927,7 +998,7 @@ func (s *Store) ReportSummary(start, end string) ([]PersonReportSummary, error)
}
func validBoardStatus(status string) bool {
return status == "backlog" || status == "in_progress" || status == "blocked" || status == "done"
return status == "backlog" || status == "todo" || status == "in_progress" || status == "blocked" || status == "done"
}
func validBoardImportance(importance string) bool {
@@ -994,7 +1065,7 @@ func (s *Store) boardTasks(archived bool) ([]BoardTask, error) {
} else {
query += ` WHERE t.archived_at IS NULL
ORDER BY CASE t.status
WHEN 'backlog' THEN 0 WHEN 'in_progress' THEN 1 WHEN 'blocked' THEN 2 ELSE 3 END,
WHEN 'backlog' THEN 0 WHEN 'todo' THEN 1 WHEN 'in_progress' THEN 2 WHEN 'blocked' THEN 3 ELSE 4 END,
t.created_at DESC`
}
rows, err := s.db.Query(query)
@@ -1069,7 +1140,28 @@ func (s *Store) boardTasks(archived bool) ([]BoardTask, error) {
tasks[index].Tags = append(tasks[index].Tags, tag)
}
}
return tasks, tagRows.Err()
if err := tagRows.Err(); err != nil {
return nil, err
}
todoRows, err := s.db.Query(`SELECT id,task_id,body,completed FROM board_task_todos
ORDER BY completed,created_at,id`)
if err != nil {
return nil, err
}
defer todoRows.Close()
for todoRows.Next() {
var todo BoardTodo
if err := todoRows.Scan(&todo.ID, &todo.TaskID, &todo.Body, &todo.Completed); err != nil {
return nil, err
}
if index, ok := taskIndex[todo.TaskID]; ok {
tasks[index].Todos = append(tasks[index].Todos, todo)
if todo.Completed {
tasks[index].TodoDone++
}
}
}
return tasks, todoRows.Err()
}
type sqlExecutor interface {
@@ -1117,6 +1209,50 @@ func (s *Store) SetBoardTaskDetails(taskID int64, assigneeIDs []int64, importanc
return tx.Commit()
}
func (s *Store) CreateBoardTaskTodo(taskID int64, body string) error {
body = strings.TrimSpace(body)
if body == "" || len([]rune(body)) > 240 {
return errors.New("to-do item must be between 1 and 240 characters")
}
result, err := s.db.Exec(`INSERT INTO board_task_todos(task_id,body) SELECT id,? FROM board_tasks WHERE id=?`, body, taskID)
if err != nil {
return err
}
count, _ := result.RowsAffected()
if count == 0 {
return errors.New("task was not found")
}
return nil
}
func (s *Store) SetBoardTaskTodoCompleted(taskID, todoID int64, completed bool) error {
value := 0
if completed {
value = 1
}
result, err := s.db.Exec(`UPDATE board_task_todos SET completed=? WHERE id=? AND task_id=?`, value, todoID, taskID)
if err != nil {
return err
}
count, _ := result.RowsAffected()
if count == 0 {
return errors.New("to-do item was not found")
}
return nil
}
func (s *Store) DeleteBoardTaskTodo(taskID, todoID int64) error {
result, err := s.db.Exec(`DELETE FROM board_task_todos WHERE id=? AND task_id=?`, todoID, taskID)
if err != nil {
return err
}
count, _ := result.RowsAffected()
if count == 0 {
return errors.New("to-do item was not found")
}
return nil
}
func (s *Store) MoveBoardTask(id int64, status string) error {
if !validBoardStatus(status) {
return errors.New("invalid board column")