feat(tasks): customizable kanban columns, task detail page, hashtag filtering and clean task list
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m0s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m0s
This commit is contained in:
1 parent
0afa2f9c2d
commit
b5df4a34b4
8 files changed
+1847
-689
No files matched your search
@@ -690,13 +690,14 @@ func (s *Server) handleUpdateThought(w http.ResponseWriter, r *http.Request) {
|
||||
KanbanStatus *string `json:"kanbanStatus"`
|
||||
Notes *string `json:"notes"`
|
||||
Subtasks *[]domain.SubTask `json:"subtasks"`
|
||||
Tags *[]string `json:"tags"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||||
return
|
||||
}
|
||||
|
||||
updated, err := s.store.UpdateThoughtDetails(userID, thoughtID, req.Content, req.IsCompleted, req.DueDate, req.Priority, req.TaskListID, req.KanbanStatus, req.Notes, req.Subtasks)
|
||||
updated, err := s.store.UpdateThoughtDetails(userID, thoughtID, req.Content, req.IsCompleted, req.DueDate, req.Priority, req.TaskListID, req.KanbanStatus, req.Notes, req.Subtasks, req.Tags)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
@@ -1590,17 +1591,18 @@ func (s *Server) handleUpdateTaskList(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
listID := chi.URLParam(r, "id")
|
||||
var req struct {
|
||||
Title *string `json:"title"`
|
||||
Icon *string `json:"icon"`
|
||||
Color *string `json:"color"`
|
||||
TeamID *string `json:"teamId"`
|
||||
SpaceID *string `json:"spaceId"`
|
||||
Title *string `json:"title"`
|
||||
Icon *string `json:"icon"`
|
||||
Color *string `json:"color"`
|
||||
TeamID *string `json:"teamId"`
|
||||
SpaceID *string `json:"spaceId"`
|
||||
Columns *[]domain.KanbanColumn `json:"columns"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||||
return
|
||||
}
|
||||
tl, err := s.store.UpdateTaskList(userID, listID, req.Title, req.Icon, req.Color, req.TeamID, req.SpaceID)
|
||||
tl, err := s.store.UpdateTaskList(userID, listID, req.Title, req.Icon, req.Color, req.TeamID, req.SpaceID, req.Columns)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
|
||||
@@ -175,6 +175,13 @@ type TaskListMember struct {
|
||||
Role string `json:"role"` // owner | member
|
||||
}
|
||||
|
||||
type KanbanColumn struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Color string `json:"color,omitempty"`
|
||||
IsDone bool `json:"isDone,omitempty"`
|
||||
}
|
||||
|
||||
type TaskList struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"userId"`
|
||||
@@ -186,6 +193,7 @@ type TaskList struct {
|
||||
IsOwner bool `json:"isOwner"`
|
||||
Members []TaskListMember `json:"members,omitempty"`
|
||||
Shares []TaskListShare `json:"shares,omitempty"`
|
||||
Columns []KanbanColumn `json:"columns,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Icon string `json:"icon"`
|
||||
Color string `json:"color"`
|
||||
|
||||
+65
-17
@@ -129,6 +129,7 @@ func (s *Store) migrate() error {
|
||||
title TEXT NOT NULL,
|
||||
icon TEXT NOT NULL DEFAULT '📋',
|
||||
color TEXT NOT NULL DEFAULT '#2563eb',
|
||||
columns TEXT NOT NULL DEFAULT '[]',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
@@ -324,6 +325,7 @@ func (s *Store) migrate() error {
|
||||
"CREATE INDEX IF NOT EXISTS idx_doc_shares_doc ON doc_shares(doc_id);",
|
||||
|
||||
"ALTER TABLE task_lists ADD COLUMN team_id TEXT NOT NULL DEFAULT '';",
|
||||
"ALTER TABLE task_lists ADD COLUMN columns TEXT NOT NULL DEFAULT '[]';",
|
||||
"CREATE TABLE IF NOT EXISTS task_list_shares (id TEXT PRIMARY KEY, task_list_id TEXT NOT NULL, target_type TEXT NOT NULL CHECK(target_type IN ('team', 'user')), target_id TEXT NOT NULL, created_at TEXT NOT NULL, created_by TEXT NOT NULL, UNIQUE(task_list_id, target_type, target_id));",
|
||||
"CREATE INDEX IF NOT EXISTS idx_task_list_shares_target ON task_list_shares(target_type, target_id);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_task_list_shares_list ON task_list_shares(task_list_id);",
|
||||
@@ -1028,7 +1030,7 @@ func (s *Store) UpdateThoughtTriage(userID, thoughtID, action, convertTo, target
|
||||
return s.GetThoughtByID(userID, thoughtID)
|
||||
}
|
||||
|
||||
func (s *Store) UpdateThoughtDetails(userID, thoughtID string, content *string, isCompleted *bool, dueDate *string, priority *string, taskListID *string, kanbanStatus *string, notes *string, subtasks *[]domain.SubTask) (*domain.Thought, error) {
|
||||
func (s *Store) UpdateThoughtDetails(userID, thoughtID string, content *string, isCompleted *bool, dueDate *string, priority *string, taskListID *string, kanbanStatus *string, notes *string, subtasks *[]domain.SubTask, tags *[]string) (*domain.Thought, error) {
|
||||
thought, err := s.GetThoughtByID(userID, thoughtID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1038,6 +1040,9 @@ func (s *Store) UpdateThoughtDetails(userID, thoughtID string, content *string,
|
||||
thought.Content = strings.TrimSpace(*content)
|
||||
thought.Tags = domain.ExtractTags(thought.Content)
|
||||
}
|
||||
if tags != nil {
|
||||
thought.Tags = *tags
|
||||
}
|
||||
if isCompleted != nil {
|
||||
thought.IsCompleted = *isCompleted
|
||||
if *isCompleted {
|
||||
@@ -1049,18 +1054,24 @@ func (s *Store) UpdateThoughtDetails(userID, thoughtID string, content *string,
|
||||
if dueDate != nil {
|
||||
thought.DueDate = *dueDate
|
||||
}
|
||||
if priority != nil && *priority != "" {
|
||||
thought.Priority = *priority
|
||||
if priority != nil {
|
||||
p := strings.ToLower(strings.TrimSpace(*priority))
|
||||
if p == "" || p == "none" {
|
||||
p = "normal"
|
||||
}
|
||||
thought.Priority = p
|
||||
}
|
||||
if taskListID != nil {
|
||||
thought.TaskListID = *taskListID
|
||||
}
|
||||
if kanbanStatus != nil && *kanbanStatus != "" {
|
||||
thought.KanbanStatus = *kanbanStatus
|
||||
if *kanbanStatus == "done" {
|
||||
thought.IsCompleted = true
|
||||
} else {
|
||||
thought.IsCompleted = false
|
||||
if isCompleted == nil {
|
||||
if *kanbanStatus == "done" {
|
||||
thought.IsCompleted = true
|
||||
} else {
|
||||
thought.IsCompleted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if notes != nil {
|
||||
@@ -2551,7 +2562,7 @@ func (s *Store) ExportUserData(userID string) (*domain.FullExport, error) {
|
||||
|
||||
// --- TASK LISTS ---
|
||||
|
||||
func (s *Store) CreateTaskList(userID, spaceID, teamID, title, icon, color string) (*domain.TaskList, error) {
|
||||
func (s *Store) CreateTaskList(userID, spaceID, teamID, title, icon, color string, columns ...[]domain.KanbanColumn) (*domain.TaskList, error) {
|
||||
if strings.TrimSpace(title) == "" {
|
||||
title = "Meine Aufgaben"
|
||||
}
|
||||
@@ -2561,6 +2572,16 @@ func (s *Store) CreateTaskList(userID, spaceID, teamID, title, icon, color strin
|
||||
if color == "" {
|
||||
color = "#2563eb"
|
||||
}
|
||||
cols := []domain.KanbanColumn{
|
||||
{ID: "todo", Name: "Zu erledigen"},
|
||||
{ID: "in_progress", Name: "In Bearbeitung", Color: "accent"},
|
||||
{ID: "waiting", Name: "Wartet auf andere", Color: "amber"},
|
||||
{ID: "done", Name: "Erledigt", IsDone: true, Color: "emerald"},
|
||||
}
|
||||
if len(columns) > 0 && len(columns[0]) > 0 {
|
||||
cols = columns[0]
|
||||
}
|
||||
colsJSON, _ := json.Marshal(cols)
|
||||
now := domain.NowISO()
|
||||
tl := &domain.TaskList{
|
||||
ID: domain.NewID("tkl"),
|
||||
@@ -2570,13 +2591,14 @@ func (s *Store) CreateTaskList(userID, spaceID, teamID, title, icon, color strin
|
||||
Title: strings.TrimSpace(title),
|
||||
Icon: icon,
|
||||
Color: color,
|
||||
Columns: cols,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
IsOwner: true,
|
||||
}
|
||||
_, err := s.db.Exec(
|
||||
`INSERT INTO task_lists (id, user_id, space_id, team_id, title, icon, color, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
tl.ID, tl.UserID, tl.SpaceID, tl.TeamID, tl.Title, tl.Icon, tl.Color, tl.CreatedAt, tl.UpdatedAt,
|
||||
`INSERT INTO task_lists (id, user_id, space_id, team_id, title, icon, color, columns, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
tl.ID, tl.UserID, tl.SpaceID, tl.TeamID, tl.Title, tl.Icon, tl.Color, string(colsJSON), tl.CreatedAt, tl.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -2729,7 +2751,7 @@ func (s *Store) getMembersForTaskList(tl *domain.TaskList) []domain.TaskListMemb
|
||||
|
||||
func (s *Store) GetTaskLists(userID string) ([]domain.TaskList, error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT id, user_id, space_id, COALESCE(team_id, ''), title, icon, color, created_at, updated_at
|
||||
`SELECT id, user_id, space_id, COALESCE(team_id, ''), title, icon, color, COALESCE(columns, '[]'), created_at, updated_at
|
||||
FROM task_lists
|
||||
WHERE user_id = ?
|
||||
OR (team_id != '' AND team_id IN (SELECT team_id FROM team_members WHERE user_id = ?))
|
||||
@@ -2746,11 +2768,23 @@ func (s *Store) GetTaskLists(userID string) ([]domain.TaskList, error) {
|
||||
return []domain.TaskList{}, err
|
||||
}
|
||||
|
||||
defaultCols := []domain.KanbanColumn{
|
||||
{ID: "todo", Name: "Zu erledigen"},
|
||||
{ID: "in_progress", Name: "In Bearbeitung", Color: "accent"},
|
||||
{ID: "waiting", Name: "Wartet auf andere", Color: "amber"},
|
||||
{ID: "done", Name: "Erledigt", IsDone: true, Color: "emerald"},
|
||||
}
|
||||
|
||||
list := make([]domain.TaskList, 0)
|
||||
for rows.Next() {
|
||||
var tl domain.TaskList
|
||||
if err := rows.Scan(&tl.ID, &tl.UserID, &tl.SpaceID, &tl.TeamID, &tl.Title, &tl.Icon, &tl.Color, &tl.CreatedAt, &tl.UpdatedAt); err == nil {
|
||||
var colsJSON string
|
||||
if err := rows.Scan(&tl.ID, &tl.UserID, &tl.SpaceID, &tl.TeamID, &tl.Title, &tl.Icon, &tl.Color, &colsJSON, &tl.CreatedAt, &tl.UpdatedAt); err == nil {
|
||||
tl.IsOwner = (tl.UserID == userID)
|
||||
_ = json.Unmarshal([]byte(colsJSON), &tl.Columns)
|
||||
if len(tl.Columns) == 0 {
|
||||
tl.Columns = defaultCols
|
||||
}
|
||||
list = append(list, tl)
|
||||
}
|
||||
}
|
||||
@@ -2779,8 +2813,9 @@ func (s *Store) GetTaskLists(userID string) ([]domain.TaskList, error) {
|
||||
|
||||
func (s *Store) GetTaskListByID(userID, listID string) (*domain.TaskList, error) {
|
||||
var tl domain.TaskList
|
||||
var colsJSON string
|
||||
err := s.db.QueryRow(
|
||||
`SELECT id, user_id, space_id, COALESCE(team_id, ''), title, icon, color, created_at, updated_at
|
||||
`SELECT id, user_id, space_id, COALESCE(team_id, ''), title, icon, color, COALESCE(columns, '[]'), created_at, updated_at
|
||||
FROM task_lists
|
||||
WHERE id = ? AND (
|
||||
user_id = ?
|
||||
@@ -2793,11 +2828,20 @@ func (s *Store) GetTaskListByID(userID, listID string) (*domain.TaskList, error)
|
||||
OR (space_id != '' AND space_id IN (SELECT space_id FROM space_members WHERE user_id = ?))
|
||||
)`,
|
||||
listID, userID, userID, userID, userID, userID,
|
||||
).Scan(&tl.ID, &tl.UserID, &tl.SpaceID, &tl.TeamID, &tl.Title, &tl.Icon, &tl.Color, &tl.CreatedAt, &tl.UpdatedAt)
|
||||
).Scan(&tl.ID, &tl.UserID, &tl.SpaceID, &tl.TeamID, &tl.Title, &tl.Icon, &tl.Color, &colsJSON, &tl.CreatedAt, &tl.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tl.IsOwner = (tl.UserID == userID)
|
||||
_ = json.Unmarshal([]byte(colsJSON), &tl.Columns)
|
||||
if len(tl.Columns) == 0 {
|
||||
tl.Columns = []domain.KanbanColumn{
|
||||
{ID: "todo", Name: "Zu erledigen"},
|
||||
{ID: "in_progress", Name: "In Bearbeitung", Color: "accent"},
|
||||
{ID: "waiting", Name: "Wartet auf andere", Color: "amber"},
|
||||
{ID: "done", Name: "Erledigt", IsDone: true, Color: "emerald"},
|
||||
}
|
||||
}
|
||||
_ = s.db.QueryRow(`SELECT COUNT(*) FROM thoughts WHERE task_list_id = ? AND is_completed = 0 AND (status = 'kept' OR (status = 'converted' AND converted_type = 'task'))`, tl.ID).Scan(&tl.TasksCount)
|
||||
_ = s.db.QueryRow(`SELECT display_name FROM users WHERE id = ?`, tl.UserID).Scan(&tl.OwnerName)
|
||||
if tl.TeamID != "" {
|
||||
@@ -2809,7 +2853,7 @@ func (s *Store) GetTaskListByID(userID, listID string) (*domain.TaskList, error)
|
||||
return &tl, nil
|
||||
}
|
||||
|
||||
func (s *Store) UpdateTaskList(userID, listID string, title, icon, color, teamID, spaceID *string) (*domain.TaskList, error) {
|
||||
func (s *Store) UpdateTaskList(userID, listID string, title, icon, color, teamID, spaceID *string, columns *[]domain.KanbanColumn) (*domain.TaskList, error) {
|
||||
tl, err := s.GetTaskListByID(userID, listID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -2826,6 +2870,9 @@ func (s *Store) UpdateTaskList(userID, listID string, title, icon, color, teamID
|
||||
if spaceID != nil {
|
||||
tl.SpaceID = *spaceID
|
||||
}
|
||||
if columns != nil {
|
||||
tl.Columns = *columns
|
||||
}
|
||||
if teamID != nil {
|
||||
oldTeamID := tl.TeamID
|
||||
tl.TeamID = *teamID
|
||||
@@ -2837,9 +2884,10 @@ func (s *Store) UpdateTaskList(userID, listID string, title, icon, color, teamID
|
||||
}
|
||||
}
|
||||
tl.UpdatedAt = domain.NowISO()
|
||||
colsJSON, _ := json.Marshal(tl.Columns)
|
||||
_, err = s.db.Exec(
|
||||
`UPDATE task_lists SET title = ?, icon = ?, color = ?, space_id = ?, team_id = ?, updated_at = ? WHERE id = ?`,
|
||||
tl.Title, tl.Icon, tl.Color, tl.SpaceID, tl.TeamID, tl.UpdatedAt, listID,
|
||||
`UPDATE task_lists SET title = ?, icon = ?, color = ?, space_id = ?, team_id = ?, columns = ?, updated_at = ? WHERE id = ?`,
|
||||
tl.Title, tl.Icon, tl.Color, tl.SpaceID, tl.TeamID, string(colsJSON), tl.UpdatedAt, listID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in new issue
Block a user