feat: task trash, triage trash and retention settings for every area; admin can forbid keeping things for good
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m39s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m39s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a927a94a95
commit
5a9c5bac37
36 files changed
+1356
-175
No files matched your search
@@ -1,23 +1,18 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Completed tasks are deleted a while after they were ticked off, so the "Erledigt" tab does not fill
|
||||
// up for good. Every user chooses the delay (done_retention_days, 0 = never); for tasks in a task list
|
||||
// the owner's choice counts, for personal tasks the user's own. thoughts.completed_at is kept by two
|
||||
// triggers, so every way of completing a task (app, Kanban, note checkbox, API) sets it.
|
||||
|
||||
// DoneRetentionChoices are the delays offered in the app, in days; 0 keeps completed tasks.
|
||||
var DoneRetentionChoices = []int{0, 7, 30, 90, 365}
|
||||
// Completed tasks move to the task trash (task_trash.go) a while after they were ticked off, so the
|
||||
// "Erledigt" tab does not fill up for good. Every user chooses the delay (done_retention_days, see
|
||||
// retention.go); for tasks in a task list the owner's choice counts, for personal tasks the user's own.
|
||||
// thoughts.completed_at is kept by two triggers, so every way of completing a task (app, Kanban, note
|
||||
// checkbox, API) sets it.
|
||||
|
||||
const DefaultDoneRetentionDays = 30
|
||||
|
||||
var ErrInvalidRetention = errors.New("Ungültige Frist für erledigte Aufgaben (erlaubt: 0, 7, 30, 90, 365 Tage)")
|
||||
|
||||
func (s *Store) migrateDoneCleanup() error {
|
||||
if err := s.addColumnIfMissing("users", "done_retention_days", "INTEGER NOT NULL DEFAULT 30"); err != nil {
|
||||
return err
|
||||
@@ -47,20 +42,16 @@ func (s *Store) DoneRetentionDays(userID string) int {
|
||||
return days
|
||||
}
|
||||
|
||||
// SetDoneRetentionDays stores one of DoneRetentionChoices; other values are refused.
|
||||
// SetDoneRetentionDays stores one of RetentionChoices; other values are refused.
|
||||
func (s *Store) SetDoneRetentionDays(userID string, days int) error {
|
||||
ok := false
|
||||
for _, d := range DoneRetentionChoices {
|
||||
ok = ok || d == days
|
||||
}
|
||||
if !ok {
|
||||
if !validRetention(days) {
|
||||
return ErrInvalidRetention
|
||||
}
|
||||
_, err := s.db.Exec(`UPDATE users SET done_retention_days = ? WHERE id = ?`, days, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeletedTask: what callers need to tell everyone who saw a deleted task.
|
||||
// DeletedTask: what callers need to tell everyone who saw a task that went to the trash.
|
||||
type DeletedTask struct {
|
||||
ID, UserID, TaskListID string
|
||||
}
|
||||
@@ -68,27 +59,29 @@ type DeletedTask struct {
|
||||
// Completed tasks: a kept task, a converted task or a task linked to a note checkbox
|
||||
const completedTaskSQL = `t.is_completed = 1 AND (t.status = 'kept' OR (t.status = 'converted' AND t.converted_type = 'task'))`
|
||||
|
||||
// PurgeExpiredDoneTasks deletes completed tasks whose delay has run out at now: the task list owner's
|
||||
// delay, or the user's own for personal tasks. A note checkbox linked to such a task stays as a plain
|
||||
// checkbox (the link lives on the task).
|
||||
func (s *Store) PurgeExpiredDoneTasks(now time.Time) ([]DeletedTask, error) {
|
||||
// PurgeExpiredDoneTasks moves completed tasks whose delay has run out at now into the task trash: the
|
||||
// task list owner's delay, or the user's own for personal tasks. A note checkbox linked to such a task
|
||||
// stays as a plain checkbox (the link lives on the task). Without allowNever, "never" counts as the
|
||||
// default delay.
|
||||
func (s *Store) PurgeExpiredDoneTasks(now time.Time, allowNever bool) ([]DeletedTask, error) {
|
||||
def := neverAs(allowNever, DefaultDoneRetentionDays)
|
||||
rows, err := s.db.Query(`
|
||||
SELECT t.id, t.user_id, t.task_list_id
|
||||
FROM thoughts t
|
||||
LEFT JOIN task_lists tl ON tl.id = t.task_list_id AND t.task_list_id != ''
|
||||
JOIN users u ON u.id = COALESCE(tl.user_id, t.user_id)
|
||||
WHERE `+completedTaskSQL+`
|
||||
AND t.completed_at != '' AND u.done_retention_days > 0
|
||||
AND t.completed_at < strftime('%Y-%m-%dT%H:%M:%SZ', ?, '-' || u.done_retention_days || ' days')`,
|
||||
now.UTC().Format("2006-01-02 15:04:05"))
|
||||
AND t.completed_at != '' AND `+daysSQL("u.done_retention_days")+` > 0
|
||||
AND t.completed_at < strftime('%Y-%m-%dT%H:%M:%SZ', ?, '-' || `+daysSQL("u.done_retention_days")+` || ' days')`,
|
||||
def, sqlNow(now), def)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.deleteTasks(rows)
|
||||
return s.trashTaskRows(rows, "")
|
||||
}
|
||||
|
||||
// ClearDoneTasks deletes the completed tasks of one task list ("" = all lists the user can see) that
|
||||
// the user may delete: own personal tasks and tasks in lists they own or edit.
|
||||
// ClearDoneTasks moves the completed tasks of one task list ("" = all lists the user can see) into the
|
||||
// task trash, as far as the user may delete them: own personal tasks and tasks in lists they own or edit.
|
||||
func (s *Store) ClearDoneTasks(userID, taskListID string) ([]DeletedTask, error) {
|
||||
if taskListID != "" {
|
||||
if err := s.ensureTaskListWritable(userID, taskListID); err != nil {
|
||||
@@ -99,7 +92,7 @@ func (s *Store) ClearDoneTasks(userID, taskListID string) ([]DeletedTask, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.deleteTasks(rows)
|
||||
return s.trashTaskRows(rows, userID)
|
||||
}
|
||||
lists, err := s.GetTaskLists(userID)
|
||||
if err != nil {
|
||||
@@ -122,15 +115,15 @@ func (s *Store) ClearDoneTasks(userID, taskListID string) ([]DeletedTask, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.deleteTasks(rows)
|
||||
return s.trashTaskRows(rows, userID)
|
||||
}
|
||||
|
||||
// deleteTasks deletes the tasks the query rows name and returns them.
|
||||
func (s *Store) deleteTasks(rows interface {
|
||||
// trashTaskRows moves the tasks the query rows name into the task trash and returns them.
|
||||
func (s *Store) trashTaskRows(rows interface {
|
||||
Next() bool
|
||||
Scan(dest ...any) error
|
||||
Close() error
|
||||
}) ([]DeletedTask, error) {
|
||||
}, byUserID string) ([]DeletedTask, error) {
|
||||
var out []DeletedTask
|
||||
for rows.Next() {
|
||||
var d DeletedTask
|
||||
@@ -140,7 +133,7 @@ func (s *Store) deleteTasks(rows interface {
|
||||
}
|
||||
_ = rows.Close()
|
||||
for _, d := range out {
|
||||
if _, err := s.db.Exec(`DELETE FROM thoughts WHERE id = ?`, d.ID); err != nil {
|
||||
if err := s.trashTask(d.ID, byUserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user