This commit is contained in:
+159
-7
@@ -103,6 +103,13 @@ type BoardTask struct {
|
||||
CreatorName string
|
||||
DueDate string
|
||||
CreatedAt string
|
||||
Tags []BoardTag
|
||||
}
|
||||
|
||||
type BoardTag struct {
|
||||
ID int64
|
||||
Name string
|
||||
Color string
|
||||
}
|
||||
|
||||
type Store struct{ db *sql.DB }
|
||||
@@ -196,6 +203,18 @@ CREATE TABLE IF NOT EXISTS board_tasks (
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_board_tasks_status ON board_tasks(status,created_at);
|
||||
CREATE TABLE IF NOT EXISTS board_tags (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
||||
color TEXT NOT NULL DEFAULT 'green',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS board_task_tags (
|
||||
task_id INTEGER NOT NULL REFERENCES board_tasks(id) ON DELETE CASCADE,
|
||||
tag_id INTEGER NOT NULL REFERENCES board_tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY(task_id,tag_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_board_task_tags_tag ON board_task_tags(tag_id,task_id);
|
||||
`
|
||||
if _, err := s.db.Exec(schema); err != nil {
|
||||
return err
|
||||
@@ -772,14 +791,14 @@ func validBoardStatus(status string) bool {
|
||||
return status == "backlog" || status == "in_progress" || status == "blocked" || status == "done"
|
||||
}
|
||||
|
||||
func (s *Store) CreateBoardTask(creatorID int64, title, description, status string, assigneeID *int64, dueDate string) error {
|
||||
func (s *Store) CreateBoardTask(creatorID int64, title, description, status string, assigneeID *int64, dueDate string) (int64, error) {
|
||||
title = strings.TrimSpace(title)
|
||||
description = strings.TrimSpace(description)
|
||||
if title == "" {
|
||||
return errors.New("task title is required")
|
||||
return 0, errors.New("task title is required")
|
||||
}
|
||||
if len([]rune(title)) > 160 || len([]rune(description)) > 4000 {
|
||||
return errors.New("task title or description is too long")
|
||||
return 0, errors.New("task title or description is too long")
|
||||
}
|
||||
if !validBoardStatus(status) {
|
||||
status = "backlog"
|
||||
@@ -792,9 +811,12 @@ func (s *Store) CreateBoardTask(creatorID int64, title, description, status stri
|
||||
if dueDate != "" {
|
||||
due = dueDate
|
||||
}
|
||||
_, err := s.db.Exec(`INSERT INTO board_tasks(title,description,status,assignee_id,creator_id,due_date)
|
||||
result, err := s.db.Exec(`INSERT INTO board_tasks(title,description,status,assignee_id,creator_id,due_date)
|
||||
VALUES(?,?,?,?,?,?)`, title, description, status, assignee, creatorID, due)
|
||||
return err
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.LastInsertId()
|
||||
}
|
||||
|
||||
func (s *Store) BoardTasks() ([]BoardTask, error) {
|
||||
@@ -811,7 +833,6 @@ func (s *Store) BoardTasks() ([]BoardTask, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var tasks []BoardTask
|
||||
for rows.Next() {
|
||||
var task BoardTask
|
||||
@@ -824,7 +845,35 @@ func (s *Store) BoardTasks() ([]BoardTask, error) {
|
||||
}
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
return tasks, rows.Err()
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
taskIndex := make(map[int64]int, len(tasks))
|
||||
for index := range tasks {
|
||||
taskIndex[tasks[index].ID] = index
|
||||
}
|
||||
tagRows, err := s.db.Query(`SELECT tt.task_id,t.id,t.name,t.color
|
||||
FROM board_task_tags tt JOIN board_tags t ON t.id=tt.tag_id
|
||||
ORDER BY t.name`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tagRows.Close()
|
||||
for tagRows.Next() {
|
||||
var taskID int64
|
||||
var tag BoardTag
|
||||
if err := tagRows.Scan(&taskID, &tag.ID, &tag.Name, &tag.Color); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if index, ok := taskIndex[taskID]; ok {
|
||||
tasks[index].Tags = append(tasks[index].Tags, tag)
|
||||
}
|
||||
}
|
||||
return tasks, tagRows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) MoveBoardTask(id int64, status string) error {
|
||||
@@ -859,3 +908,106 @@ func (s *Store) DeleteBoardTask(id, userID int64, admin bool) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validTagColor(color string) bool {
|
||||
switch color {
|
||||
case "green", "blue", "red", "amber", "purple", "slate":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) CreateBoardTag(name, color string) error {
|
||||
name = strings.TrimSpace(name)
|
||||
color = strings.TrimSpace(color)
|
||||
if name == "" || len([]rune(name)) > 40 {
|
||||
return errors.New("tag name must be between 1 and 40 characters")
|
||||
}
|
||||
if !validTagColor(color) {
|
||||
return errors.New("choose a valid tag color")
|
||||
}
|
||||
_, err := s.db.Exec(`INSERT INTO board_tags(name,color) VALUES(?,?)`, name, color)
|
||||
if err != nil && strings.Contains(strings.ToLower(err.Error()), "unique") {
|
||||
return errors.New("a tag with that name already exists")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) FindOrCreateBoardTag(name, color string) (BoardTag, error) {
|
||||
name = strings.TrimSpace(name)
|
||||
color = strings.TrimSpace(color)
|
||||
if name == "" || len([]rune(name)) > 40 {
|
||||
return BoardTag{}, errors.New("tag name must be between 1 and 40 characters")
|
||||
}
|
||||
if !validTagColor(color) {
|
||||
return BoardTag{}, errors.New("choose a valid tag color")
|
||||
}
|
||||
if _, err := s.db.Exec(`INSERT INTO board_tags(name,color) VALUES(?,?)
|
||||
ON CONFLICT(name) DO NOTHING`, name, color); err != nil {
|
||||
return BoardTag{}, err
|
||||
}
|
||||
var tag BoardTag
|
||||
err := s.db.QueryRow(`SELECT id,name,color FROM board_tags
|
||||
WHERE name = ? COLLATE NOCASE`, name).Scan(&tag.ID, &tag.Name, &tag.Color)
|
||||
return tag, err
|
||||
}
|
||||
|
||||
func (s *Store) BoardTags() ([]BoardTag, error) {
|
||||
rows, err := s.db.Query(`SELECT id,name,color FROM board_tags ORDER BY name`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var tags []BoardTag
|
||||
for rows.Next() {
|
||||
var tag BoardTag
|
||||
if err := rows.Scan(&tag.ID, &tag.Name, &tag.Color); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
return tags, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) SetBoardTaskTags(taskID int64, tagIDs []int64) error {
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
var exists int
|
||||
if err := tx.QueryRow(`SELECT COUNT(*) FROM board_tasks WHERE id=?`, taskID).Scan(&exists); err != nil {
|
||||
return err
|
||||
}
|
||||
if exists == 0 {
|
||||
return errors.New("task was not found")
|
||||
}
|
||||
if _, err := tx.Exec(`DELETE FROM board_task_tags WHERE task_id=?`, taskID); err != nil {
|
||||
return err
|
||||
}
|
||||
seen := map[int64]bool{}
|
||||
for _, tagID := range tagIDs {
|
||||
if tagID < 1 || seen[tagID] {
|
||||
continue
|
||||
}
|
||||
seen[tagID] = true
|
||||
if _, err := tx.Exec(`INSERT INTO board_task_tags(task_id,tag_id)
|
||||
SELECT ?,id FROM board_tags WHERE id=?`, taskID, tagID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (s *Store) DeleteBoardTag(id int64) error {
|
||||
result, err := s.db.Exec(`DELETE FROM board_tags WHERE id=?`, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
count, _ := result.RowsAffected()
|
||||
if count == 0 {
|
||||
return errors.New("tag was not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user