feat: expand kanban assignment and filtering
Test and publish / verify (push) Successful in 2m22s

This commit is contained in:
2026-07-29 13:53:31 +03:30
parent e1475d0df0
commit 5a3ca4eb40
7 changed files with 432 additions and 91 deletions
+79 -5
View File
@@ -40,7 +40,7 @@ func TestExistingDatabaseAddsNewColumns(t *testing.T) {
due_date TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO board_tasks(title,creator_id) VALUES('Legacy card',1)`)
INSERT INTO board_tasks(title,assignee_id,creator_id) VALUES('Legacy card',1,1)`)
if err != nil {
t.Fatal(err)
}
@@ -67,6 +67,41 @@ func TestExistingDatabaseAddsNewColumns(t *testing.T) {
if archivedAt.Valid {
t.Fatalf("legacy card was unexpectedly archived: %q", archivedAt.String)
}
var importance string
if err := store.db.QueryRow(`SELECT importance FROM board_tasks WHERE title='Legacy card'`).Scan(&importance); err != nil || importance != "normal" {
t.Fatalf("legacy card importance: %q, %v", importance, err)
}
var migratedAssignees int
if err := store.db.QueryRow(`SELECT COUNT(*) FROM board_task_assignees WHERE task_id=1 AND user_id=1`).Scan(&migratedAssignees); err != nil || migratedAssignees != 1 {
t.Fatalf("legacy card assignees: %d, %v", migratedAssignees, err)
}
}
func TestBoardTasksSortByCreationDate(t *testing.T) {
store, err := OpenStore(filepath.Join(t.TempDir(), "sort.db"))
if err != nil {
t.Fatal(err)
}
defer store.Close()
olderID, err := store.CreateBoardTask(1, "Created later", "", "backlog", "normal", nil, "")
if err != nil {
t.Fatal(err)
}
newerID, err := store.CreateBoardTask(1, "Created earlier", "", "backlog", "normal", nil, "")
if err != nil {
t.Fatal(err)
}
if _, err := store.db.Exec(`UPDATE board_tasks SET created_at=CASE id WHEN ? THEN ? ELSE ? END`,
olderID, "2026-07-29 12:00:00", "2026-07-29 11:00:00"); err != nil {
t.Fatal(err)
}
tasks, err := store.BoardTasks()
if err != nil {
t.Fatal(err)
}
if len(tasks) != 2 || tasks[0].ID != olderID || tasks[1].ID != newerID {
t.Fatalf("cards were not sorted newest-created-first: %#v", tasks)
}
}
func TestLoginAttendanceAndReportFlow(t *testing.T) {
@@ -642,8 +677,12 @@ func TestSharedBoardCreateAssignMoveAndPermissions(t *testing.T) {
"title": {"Ship the onboarding flow"},
"description": {"Finish QA and publish the release."},
"status": {"backlog"},
"assignee_id": {strconv.FormatInt(memberID, 10)},
"due_date": {"1405-05-06"},
"assignee_ids": {
"1",
strconv.FormatInt(memberID, 10),
},
"importance": {"high"},
"due_date": {"1405-05-06"},
"tag_ids": {
strconv.FormatInt(tags[0].ID, 10),
strconv.FormatInt(tags[1].ID, 10),
@@ -658,7 +697,7 @@ func TestSharedBoardCreateAssignMoveAndPermissions(t *testing.T) {
if err != nil || len(tasks) != 1 {
t.Fatalf("board tasks after create: %#v, %v", tasks, err)
}
if tasks[0].AssigneeName != "Board Member" || tasks[0].DueDate != "2026-07-28" {
if len(tasks[0].Assignees) != 2 || tasks[0].Importance != "high" || tasks[0].DueDate != "2026-07-28" {
t.Fatalf("unexpected assigned board task: %#v", tasks[0])
}
if len(tasks[0].Tags) != 3 {
@@ -677,7 +716,7 @@ func TestSharedBoardCreateAssignMoveAndPermissions(t *testing.T) {
if boardResponse.Code != http.StatusOK {
t.Fatalf("board page status: %d body %s", boardResponse.Code, boardResponse.Body.String())
}
for _, expected := range []string{"Ship the onboarding flow", "Board Member", "1405-05-06", "Manage tags", "Tags", "select multiple", "Create a new tag", "Backend", "Frontend", "Urgent", "/static/board.js", `data-board-column="backlog"`} {
for _, expected := range []string{"Ship the onboarding flow", "Board Member", "Workspace Admin", "1405-05-06", "High", "Manage tags", "Tags", "select multiple", "Create a new tag", "People & importance", "Card controls", "Backend", "Frontend", "Urgent", "/static/board.js", `data-board-column="backlog"`} {
if !strings.Contains(boardResponse.Body.String(), expected) {
t.Fatalf("board page does not include %q", expected)
}
@@ -704,6 +743,41 @@ func TestSharedBoardCreateAssignMoveAndPermissions(t *testing.T) {
if len(tasks[0].Tags) != 1 || tasks[0].Tags[0].ID != tags[0].ID {
t.Fatalf("task tags were not updated: %#v", tasks[0].Tags)
}
detailsPath := "/board/tasks/" + strconv.FormatInt(tasks[0].ID, 10) + "/details"
setDetails := formRequest(t, s.http.Handler, detailsPath, url.Values{
"csrf": {memberCSRF},
"assignee_ids": {strconv.FormatInt(memberID, 10)},
"importance": {"urgent"},
}, memberCookie)
if setDetails.Code != http.StatusSeeOther || setDetails.Header().Get("Location") != "/board?flash=Card+details+updated" {
t.Fatalf("set task details: got %d location %q", setDetails.Code, setDetails.Header().Get("Location"))
}
tasks, _ = s.store.BoardTasks()
if len(tasks[0].Assignees) != 1 || tasks[0].Assignees[0].ID != memberID || tasks[0].Importance != "urgent" {
t.Fatalf("task people or importance were not updated: %#v", tasks[0])
}
matchingFilterPath := "/board?tag=" + strconv.FormatInt(tags[0].ID, 10) +
"&assignee=" + strconv.FormatInt(memberID, 10) + "&priority=urgent"
matchingFilterRequest := httptest.NewRequest(http.MethodGet, matchingFilterPath, nil)
matchingFilterRequest.AddCookie(memberCookie)
matchingFilter := httptest.NewRecorder()
s.http.Handler.ServeHTTP(matchingFilter, matchingFilterRequest)
if matchingFilter.Code != http.StatusOK || !strings.Contains(matchingFilter.Body.String(), "Ship the onboarding flow") || !strings.Contains(matchingFilter.Body.String(), "Clear") {
t.Fatalf("matching board filter: %d %s", matchingFilter.Code, matchingFilter.Body.String())
}
for _, path := range []string{
"/board?priority=high",
"/board?assignee=1",
"/board?tag=999999",
} {
request := httptest.NewRequest(http.MethodGet, path, nil)
request.AddCookie(memberCookie)
response := httptest.NewRecorder()
s.http.Handler.ServeHTTP(response, request)
if response.Code != http.StatusOK || strings.Contains(response.Body.String(), "Ship the onboarding flow") || !strings.Contains(response.Body.String(), "No matching cards") {
t.Fatalf("non-matching board filter %q: %d %s", path, response.Code, response.Body.String())
}
}
deleteTagPath := "/board/tags/" + strconv.FormatInt(tags[1].ID, 10) + "/delete"
deleteTag := formRequest(t, s.http.Handler, deleteTagPath, url.Values{"csrf": {adminCSRF}}, adminCookie)