Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions apps/backend/internal/backendapp/boot_state_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,19 @@ func (b bootStateBuilder) tasksPageBootData(ctx context.Context, req *http.Reque
settingsWorkspaceID := ""
settingsWorkflowID := ""
settingsRepositoryID := ""
settingsTasksListSort := ""
settingsTasksListGroup := ""
if hasSettings {
settingsWorkspaceID = settings.Settings.WorkspaceID
settingsWorkflowID = settings.Settings.WorkflowFilterID
settingsTasksListSort = settings.Settings.TasksListSort
settingsTasksListGroup = settings.Settings.TasksListGroup
if len(settings.Settings.RepositoryIDs) > 0 {
settingsRepositoryID = settings.Settings.RepositoryIDs[0]
}
}
tasksListSort := tasksListSortForRoute(queryValue(req, "sort"), settingsTasksListSort)
tasksListGroup := tasksListGroupForRoute(queryValue(req, "group"), settingsTasksListGroup)
workspaceIDs := workspaceIDSet(workspaces)
activeWorkspaceID := firstValidID(
workspaceIDs,
Expand All @@ -54,7 +60,7 @@ func (b bootStateBuilder) tasksPageBootData(ctx context.Context, req *http.Reque
state["userSettings"] = mapUserSettingsState(settings, activeWorkspaceID)
}
if activeWorkspaceID == "" {
return state, map[string]any{"activeWorkspaceId": nil, "workflows": []any{}, "steps": []any{}, "repositories": []any{}, "tasks": []any{}, "total": 0}
return state, map[string]any{"activeWorkspaceId": nil, "workflows": []any{}, "steps": []any{}, "repositories": []any{}, "tasks": []any{}, "total": 0, "tasksListSort": tasksListSort, "tasksListGroup": tasksListGroup}
}
workflows, err := b.p.taskSvc.ListWorkflows(ctx, activeWorkspaceID, false)
if err != nil {
Expand All @@ -69,14 +75,16 @@ func (b bootStateBuilder) tasksPageBootData(ctx context.Context, req *http.Reque
}
repositories := b.repositoriesForState(ctx, activeWorkspaceID, state)
steps := b.workflowStepsForWorkspace(ctx, activeWorkspaceID)
tasks, total := b.tasksForWorkspace(ctx, activeWorkspaceID, activeWorkflowID, settingsRepositoryID)
tasks, total := b.tasksForWorkspace(ctx, activeWorkspaceID, activeWorkflowID, settingsRepositoryID, tasksListSort)
routeData := map[string]any{
"activeWorkspaceId": activeWorkspaceID,
"workflows": workflowsToDTOs(workflows),
"steps": steps,
"repositories": repositories,
"tasks": tasks,
"total": total,
"tasksListSort": tasksListSort,
"tasksListGroup": tasksListGroup,
}
return state, routeData
}
Expand Down Expand Up @@ -237,8 +245,8 @@ func (b bootStateBuilder) workflowStepsForWorkspace(ctx context.Context, workspa
return items
}

func (b bootStateBuilder) tasksForWorkspace(ctx context.Context, workspaceID, workflowID, repositoryID string) ([]taskdto.TaskDTO, int) {
tasks, total, err := b.p.taskSvc.ListTasksByWorkspace(ctx, workspaceID, workflowID, repositoryID, "", 1, 25, false, false, false, false)
func (b bootStateBuilder) tasksForWorkspace(ctx context.Context, workspaceID, workflowID, repositoryID, sort string) ([]taskdto.TaskDTO, int) {
tasks, total, err := b.p.taskSvc.ListTasksByWorkspace(ctx, workspaceID, workflowID, repositoryID, "", 1, 25, sort, false, false, false, false)
if err != nil {
b.logBootError("list tasks page tasks", err)
return []taskdto.TaskDTO{}, 0
Expand Down Expand Up @@ -438,6 +446,8 @@ func mapUserSettingsState(response userdto.UserSettingsResponse, workspaceID str
"kanbanViewMode": nullString(settings.KanbanViewMode),
"workflowId": nullString(settings.WorkflowFilterID),
"repositoryIds": stringSlice(settings.RepositoryIDs),
"tasksListSort": usermodels.NormalizeTasksListSort(settings.TasksListSort),
"tasksListGroup": usermodels.NormalizeTasksListGroup(settings.TasksListGroup),
"preferredShell": nullString(settings.PreferredShell),
"shellOptions": response.ShellOptions,
"defaultEditorId": nullString(settings.DefaultEditorID),
Expand Down Expand Up @@ -635,6 +645,20 @@ func queryValue(req *http.Request, name string) string {
return strings.TrimSpace(parsed.Query().Get(name))
}

func tasksListSortForRoute(queryValue, settingsValue string) string {
if usermodels.IsValidTasksListSort(queryValue) {
return strings.TrimSpace(queryValue)
}
return usermodels.NormalizeTasksListSort(settingsValue)
}

func tasksListGroupForRoute(queryValue, settingsValue string) string {
if usermodels.IsValidTasksListGroup(queryValue) {
return strings.TrimSpace(queryValue)
}
return usermodels.NormalizeTasksListGroup(settingsValue)
}

func readActiveWorkspaceCookie(req *http.Request) string {
if req == nil {
return ""
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/internal/backendapp/e2e_reset.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ func handleE2EReset(
// accumulate across tests in the same Playwright worker and
// eventually exhaust the per-worker port range.
const resetPageSize = 10000
tasks, total, err := repo.ListTasksByWorkspace(ctx, workspaceID, "", "", "", 1, resetPageSize, true, true, false, false)
tasks, total, err := repo.ListTasksByWorkspace(ctx, workspaceID, "", "", "", 1, resetPageSize, "", true, true, false, false)
if err != nil {
log.Error("e2e reset: failed to list tasks", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{errKey: err.Error()})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func (s *stubTaskWorkspace) DeleteWorkspace(_ context.Context, _ string) error {
return nil
}
func (s *stubTaskWorkspace) ListTasksByWorkspace(
_ context.Context, _, _, _, _ string, _, _ int, _, _, _, _ bool,
_ context.Context, _, _, _, _ string, _, _ int, _ string, _, _, _, _ bool,
) ([]*taskmodels.Task, int, error) {
return nil, 0, nil
}
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/internal/office/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ type TaskWorkspaceService interface {
GetWorkspace(ctx context.Context, id string) (*taskmodels.Workspace, error)
ListWorkspaces(ctx context.Context) ([]*taskmodels.Workspace, error)
DeleteWorkspace(ctx context.Context, id string) error
ListTasksByWorkspace(ctx context.Context, workspaceID, workflowID, repositoryID, query string, page, pageSize int, includeArchived, includeEphemeral, onlyEphemeral, excludeConfig bool) ([]*taskmodels.Task, int, error)
ListTasksByWorkspace(ctx context.Context, workspaceID, workflowID, repositoryID, query string, page, pageSize int, sort string, includeArchived, includeEphemeral, onlyEphemeral, excludeConfig bool) ([]*taskmodels.Task, int, error)
DeleteTask(ctx context.Context, id string) error
GetLastAgentMessage(ctx context.Context, sessionID string) (string, error)
GetLastAgentMessageForTurn(ctx context.Context, turnID string) (string, error)
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/internal/office/service/workspace_deletion.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (s *Service) GetWorkspaceDeletionSummary(ctx context.Context, workspaceID s
if err != nil {
return nil, err
}
_, total, err := s.taskWorkspace.ListTasksByWorkspace(ctx, workspaceID, "", "", "", 1, 1, true, true, false, false)
_, total, err := s.taskWorkspace.ListTasksByWorkspace(ctx, workspaceID, "", "", "", 1, 1, "", true, true, false, false)
if err != nil {
return nil, fmt.Errorf("count tasks: %w", err)
}
Expand Down Expand Up @@ -113,7 +113,7 @@ func (s *Service) listAllWorkspaceTasks(ctx context.Context, workspaceID string)
var all []*taskmodels.Task
for page := 1; ; page++ {
tasks, total, err := s.taskWorkspace.ListTasksByWorkspace(
ctx, workspaceID, "", "", "", page, workspaceDeletionPageSize, true, true, false, false,
ctx, workspaceID, "", "", "", page, workspaceDeletionPageSize, "", true, true, false, false,
)
if err != nil {
return nil, fmt.Errorf("list workspace tasks: %w", err)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ func (f *fakeWorkspaceTaskService) ListTasksByWorkspace(
string,
int,
int,
string,
bool,
bool,
bool,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ func (m *mockRepository) DeleteTask(ctx context.Context, id string) error
func (m *mockRepository) ListTasks(ctx context.Context, workflowID string) ([]*models.Task, error) {
return nil, nil
}
func (m *mockRepository) ListTasksByWorkspace(ctx context.Context, workspaceID, workflowID, repositoryID, query string, page, pageSize int, includeArchived, includeEphemeral, onlyEphemeral, excludeConfig bool) ([]*models.Task, int, error) {
func (m *mockRepository) ListTasksByWorkspace(ctx context.Context, workspaceID, workflowID, repositoryID, query string, page, pageSize int, sort string, includeArchived, includeEphemeral, onlyEphemeral, excludeConfig bool) ([]*models.Task, int, error) {
return nil, 0, nil
}
func (m *mockRepository) ListTasksByWorkflowStep(ctx context.Context, workflowStepID string) ([]*models.Task, error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func (m *mockRepository) DeleteTask(ctx context.Context, id string) error {
func (m *mockRepository) ListTasks(ctx context.Context, workflowID string) ([]*models.Task, error) {
return nil, nil
}
func (m *mockRepository) ListTasksByWorkspace(ctx context.Context, workspaceID, workflowID, repositoryID, query string, page, pageSize int, includeArchived, includeEphemeral, onlyEphemeral, excludeConfig bool) ([]*models.Task, int, error) {
func (m *mockRepository) ListTasksByWorkspace(ctx context.Context, workspaceID, workflowID, repositoryID, query string, page, pageSize int, sort string, includeArchived, includeEphemeral, onlyEphemeral, excludeConfig bool) ([]*models.Task, int, error) {
return nil, 0, nil
}
func (m *mockRepository) ListTasksByWorkflowStep(ctx context.Context, workflowStepID string) ([]*models.Task, error) {
Expand Down
3 changes: 2 additions & 1 deletion apps/backend/internal/task/handlers/task_http_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ func (h *TaskHandlers) httpListTasksByWorkspace(c *gin.Context) {
}

query := c.Query("query")
sort := usermodels.NormalizeTasksListSort(c.Query("sort"))
workflowID := c.Query("workflow_id")
repositoryID := c.Query("repository_id")
includeArchived := c.Query("include_archived") == queryValueTrue
Expand All @@ -61,7 +62,7 @@ func (h *TaskHandlers) httpListTasksByWorkspace(c *gin.Context) {
excludeConfig := c.Query("exclude_config") == queryValueTrue

tasks, total, err := h.service.ListTasksByWorkspace(
c.Request.Context(), c.Param("id"), workflowID, repositoryID, query, page, pageSize, includeArchived, includeEphemeral, onlyEphemeral, excludeConfig,
c.Request.Context(), c.Param("id"), workflowID, repositoryID, query, page, pageSize, sort, includeArchived, includeEphemeral, onlyEphemeral, excludeConfig,
)
if err != nil {
handleNotFound(c, h.logger, err, "tasks not found")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func TestSQLiteRepository_ListTasksByWorkspace_IncludeArchived(t *testing.T) {
_ = repo.ArchiveTask(ctx, "task-2")

// Without includeArchived: should return only active task
tasks, total, err := repo.ListTasksByWorkspace(ctx, "ws-1", "", "", "", 1, 10, false, false, false, false)
tasks, total, err := repo.ListTasksByWorkspace(ctx, "ws-1", "", "", "", 1, 10, "", false, false, false, false)
if err != nil {
t.Fatalf("failed to list tasks: %v", err)
}
Expand All @@ -126,7 +126,7 @@ func TestSQLiteRepository_ListTasksByWorkspace_IncludeArchived(t *testing.T) {
}

// With includeArchived: should return both tasks
tasks, total, err = repo.ListTasksByWorkspace(ctx, "ws-1", "", "", "", 1, 10, true, false, false, false)
tasks, total, err = repo.ListTasksByWorkspace(ctx, "ws-1", "", "", "", 1, 10, "", true, false, false, false)
if err != nil {
t.Fatalf("failed to list tasks with archived: %v", err)
}
Expand All @@ -138,7 +138,7 @@ func TestSQLiteRepository_ListTasksByWorkspace_IncludeArchived(t *testing.T) {
}

// Search with includeArchived=false should filter archived
_, total, err = repo.ListTasksByWorkspace(ctx, "ws-1", "", "", "Task", 1, 10, false, false, false, false)
_, total, err = repo.ListTasksByWorkspace(ctx, "ws-1", "", "", "Task", 1, 10, "", false, false, false, false)
if err != nil {
t.Fatalf("failed to search tasks: %v", err)
}
Expand All @@ -147,7 +147,7 @@ func TestSQLiteRepository_ListTasksByWorkspace_IncludeArchived(t *testing.T) {
}

// Search with includeArchived=true should include archived
_, total, err = repo.ListTasksByWorkspace(ctx, "ws-1", "", "", "Task", 1, 10, true, false, false, false)
_, total, err = repo.ListTasksByWorkspace(ctx, "ws-1", "", "", "Task", 1, 10, "", true, false, false, false)
if err != nil {
t.Fatalf("failed to search tasks with archived: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/internal/task/repository/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type TaskRepository interface {
UpdateTask(ctx context.Context, task *models.Task) error
DeleteTask(ctx context.Context, id string) error
ListTasks(ctx context.Context, workflowID string) ([]*models.Task, error)
ListTasksByWorkspace(ctx context.Context, workspaceID, workflowID, repositoryID, query string, page, pageSize int, includeArchived, includeEphemeral, onlyEphemeral, excludeConfig bool) ([]*models.Task, int, error)
ListTasksByWorkspace(ctx context.Context, workspaceID, workflowID, repositoryID, query string, page, pageSize int, sort string, includeArchived, includeEphemeral, onlyEphemeral, excludeConfig bool) ([]*models.Task, int, error)
ListTasksByWorkflowStep(ctx context.Context, workflowStepID string) ([]*models.Task, error)
ArchiveTask(ctx context.Context, id string) error
// ArchiveTaskIfActive is the CAS variant used by office task-handoffs
Expand Down
38 changes: 30 additions & 8 deletions apps/backend/internal/task/repository/sqlite/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/kandev/kandev/internal/agentctl/tracing"
"github.com/kandev/kandev/internal/db/dialect"
"github.com/kandev/kandev/internal/task/models"
usermodels "github.com/kandev/kandev/internal/user/models"
v1 "github.com/kandev/kandev/pkg/api/v1"
)

Expand Down Expand Up @@ -414,14 +415,15 @@ func (r *Repository) ListTasksByWorkflowStep(ctx context.Context, workflowStepID
// If includeArchived is false, archived tasks are excluded
// If includeEphemeral is false, ephemeral tasks are excluded
// If onlyEphemeral is true, only ephemeral tasks are returned
func (r *Repository) ListTasksByWorkspace(ctx context.Context, workspaceID, workflowID, repositoryID, query string, page, pageSize int, includeArchived, includeEphemeral, onlyEphemeral, excludeConfig bool) ([]*models.Task, int, error) {
func (r *Repository) ListTasksByWorkspace(ctx context.Context, workspaceID, workflowID, repositoryID, query string, page, pageSize int, sort string, includeArchived, includeEphemeral, onlyEphemeral, excludeConfig bool) ([]*models.Task, int, error) {
ctx, span := tracing.Tracer("kandev-db").Start(ctx, "db.ListTasksByWorkspace")
defer span.End()
// Calculate offset
offset := (page - 1) * pageSize
if offset < 0 {
offset = 0
}
sort = usermodels.NormalizeTasksListSort(sort)

// Build filter conditions
filter := ""
Expand All @@ -447,9 +449,9 @@ func (r *Repository) ListTasksByWorkspace(ctx context.Context, workspaceID, work
var err error

if query == "" {
rows, total, err = r.queryAllTasks(ctx, workspaceID, filter, workflowID, repositoryID, pageSize, offset)
rows, total, err = r.queryAllTasks(ctx, workspaceID, filter, workflowID, repositoryID, pageSize, offset, sort)
} else {
rows, total, err = r.searchTasks(ctx, workspaceID, query, filter, workflowID, repositoryID, pageSize, offset, includeArchived, includeEphemeral, onlyEphemeral, excludeConfig)
rows, total, err = r.searchTasks(ctx, workspaceID, query, filter, workflowID, repositoryID, pageSize, offset, sort, includeArchived, includeEphemeral, onlyEphemeral, excludeConfig)
}

if err != nil {
Expand All @@ -466,7 +468,7 @@ func (r *Repository) ListTasksByWorkspace(ctx context.Context, workspaceID, work
}

// queryAllTasks fetches all tasks (no search) for a workspace with pagination.
func (r *Repository) queryAllTasks(ctx context.Context, workspaceID, taskFilter, workflowID, repositoryID string, pageSize, offset int) (*sql.Rows, int, error) {
func (r *Repository) queryAllTasks(ctx context.Context, workspaceID, taskFilter, workflowID, repositoryID string, pageSize, offset int, sort string) (*sql.Rows, int, error) {
args := []interface{}{workspaceID}
if workflowID != "" {
taskFilter += " AND workflow_id = ?"
Expand All @@ -484,12 +486,32 @@ func (r *Repository) queryAllTasks(ctx context.Context, workspaceID, taskFilter,
SELECT `+taskSelectColumns("t")+`
FROM tasks t
WHERE t.workspace_id = ?`+rewriteFilterForAlias(taskFilter, "t")+`
ORDER BY t.updated_at DESC
ORDER BY `+taskListOrderBy("t", sort)+`
LIMIT ? OFFSET ?
`), append(append([]interface{}{}, args...), pageSize, offset)...)
return rows, total, err
}

func taskListOrderBy(alias, sort string) string {
prefix := alias + "."
switch sort {
case usermodels.TasksListSortUpdatedAsc:
return prefix + "updated_at ASC, " + prefix + "title COLLATE NOCASE ASC, " + prefix + "id ASC"
case usermodels.TasksListSortCreatedDesc:
return prefix + "created_at DESC, " + prefix + "title COLLATE NOCASE ASC, " + prefix + "id ASC"
case usermodels.TasksListSortCreatedAsc:
return prefix + "created_at ASC, " + prefix + "title COLLATE NOCASE ASC, " + prefix + "id ASC"
case usermodels.TasksListSortTitleAsc:
return prefix + "title COLLATE NOCASE ASC, " + prefix + "updated_at DESC, " + prefix + "id ASC"
case usermodels.TasksListSortTitleDesc:
return prefix + "title COLLATE NOCASE DESC, " + prefix + "updated_at DESC, " + prefix + "id ASC"
case usermodels.TasksListSortUpdatedDesc:
fallthrough
default:
return prefix + "updated_at DESC, " + prefix + "title COLLATE NOCASE ASC, " + prefix + "id ASC"
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated

// rewriteFilterForAlias prefixes bare column references in `filter` with
// the given alias. Only used for the small, controlled fragments built
// in queryAllTasks and searchTasks.
Expand Down Expand Up @@ -557,7 +579,7 @@ func isWordByte(b byte) bool {
}

// searchTasks fetches tasks matching a search query for a workspace with pagination.
func (r *Repository) searchTasks(ctx context.Context, workspaceID, query, filter, workflowID, repositoryID string, pageSize, offset int, includeArchived, includeEphemeral, onlyEphemeral, excludeConfig bool) (*sql.Rows, int, error) {
func (r *Repository) searchTasks(ctx context.Context, workspaceID, query, filter, workflowID, repositoryID string, pageSize, offset int, sort string, includeArchived, includeEphemeral, onlyEphemeral, excludeConfig bool) (*sql.Rows, int, error) {
searchPattern := "%" + query + "%"
like := dialect.Like(r.ro.DriverName())

Expand Down Expand Up @@ -616,9 +638,9 @@ func (r *Repository) searchTasks(ctx context.Context, workspaceID, query, filter
r.name %s ? OR
r.local_path %s ?
)
ORDER BY t.updated_at DESC
ORDER BY %s
LIMIT ? OFFSET ?
`, taskSelectColumns("t"), tFilter, like, like, like, like)
`, taskSelectColumns("t"), tFilter, like, like, like, like, taskListOrderBy("t", sort))
selectArgs := append(append([]interface{}{}, countArgs...), pageSize, offset)
rows, err := r.ro.QueryContext(ctx, r.ro.Rebind(selectQuery), selectArgs...)
return rows, total, err
Expand Down
Loading
Loading