feat: sort tasks by creation date in kanban and sidebar (#511)

This commit is contained in:
Carlos Florêncio
2026-03-31 23:31:18 +01:00
committed by GitHub
parent c732675f33
commit f4723ebcae
12 changed files with 55 additions and 22 deletions
@@ -129,7 +129,7 @@ func (r *Repository) ListTasks(ctx context.Context, workflowID string) ([]*model
SELECT id, workspace_id, workflow_id, workflow_step_id, title, description, state, priority, position, metadata, is_ephemeral, parent_id, archived_at, created_at, updated_at
FROM tasks
WHERE workflow_id = ? AND archived_at IS NULL AND is_ephemeral = 0
ORDER BY position
ORDER BY created_at ASC
`), workflowID)
if err != nil {
return nil, err
@@ -164,7 +164,7 @@ func (r *Repository) ListTasksByWorkflowStep(ctx context.Context, workflowStepID
rows, err := r.ro.QueryContext(ctx, r.ro.Rebind(`
SELECT id, workspace_id, workflow_id, workflow_step_id, title, description, state, priority, position, metadata, is_ephemeral, parent_id, archived_at, created_at, updated_at
FROM tasks
WHERE workflow_step_id = ? AND archived_at IS NULL AND is_ephemeral = 0 ORDER BY position
WHERE workflow_step_id = ? AND archived_at IS NULL AND is_ephemeral = 0 ORDER BY created_at ASC
`), workflowStepID)
if err != nil {
return nil, err
+1 -1
View File
@@ -59,7 +59,7 @@ function getTasksForStep(tasks: Task[], stepId: string) {
return tasks
.filter((task) => task.workflowStepId === stepId)
.map((task) => ({ ...task, position: task.position ?? 0 }))
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
.sort((a, b) => (a.createdAt ?? "").localeCompare(b.createdAt ?? ""));
}
function EmptyState({ showLoading }: { showLoading: boolean }) {
+1
View File
@@ -59,6 +59,7 @@ export interface Task {
isRemoteExecutor?: boolean;
parentTaskId?: string | null;
updatedAt?: string;
createdAt?: string;
}
export interface WorkflowStep {
@@ -194,7 +194,7 @@ function useSwimlaneGraphDnd({ tasks, steps, workflowId, onMoveError }: Swimlane
for (const col of steps) {
map[col.id] = tasks
.filter((t) => t.workflowStepId === col.id)
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
.sort((a, b) => (a.createdAt ?? "").localeCompare(b.createdAt ?? ""));
}
return map;
}, [steps, tasks]);
@@ -79,9 +79,8 @@ function useSwimlaneKanbanDnd({ tasks, workflowId, onMoveError }: SwimlaneKanban
.filter(
(t: KanbanState["tasks"][number]) => t.workflowStepId === targetStepId && t.id !== taskId,
)
.sort(
(a: KanbanState["tasks"][number], b: KanbanState["tasks"][number]) =>
a.position - b.position,
.sort((a: KanbanState["tasks"][number], b: KanbanState["tasks"][number]) =>
(a.createdAt ?? "").localeCompare(b.createdAt ?? ""),
);
const nextPosition = targetTasks.length;
const originalTasks = snapshot.tasks;
@@ -164,7 +163,7 @@ function useTasksByStep(tasks: Task[]) {
(stepId: string) =>
tasks
.filter((t) => t.workflowStepId === stepId)
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0)),
.sort((a, b) => (a.createdAt ?? "").localeCompare(b.createdAt ?? "")),
[tasks],
);
}
@@ -55,7 +55,7 @@ export function SwipeableColumns({
return tasks
.filter((task) => task.workflowStepId === stepId)
.map((task) => ({ ...task, position: task.position ?? 0 }))
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
.sort((a, b) => (a.createdAt ?? "").localeCompare(b.createdAt ?? ""));
},
[tasks],
);
@@ -143,6 +143,7 @@ function toSidebarItem(
remoteExecutorName: task.primaryExecutorName ?? undefined,
primarySessionId: task.primarySessionId ?? null,
updatedAt: sessionInfo.updatedAt ?? task.updatedAt,
createdAt: task.createdAt,
isArchived: false as boolean,
parentTaskTitle: task.parentTaskId ? ctx.titleById.get(task.parentTaskId) : undefined,
};
@@ -232,6 +233,7 @@ function useSidebarData(workspaceId: string | null) {
remoteExecutorName: undefined,
primarySessionId: null,
updatedAt: archivedState.archivedTaskUpdatedAt,
createdAt: undefined,
isArchived: true,
parentTaskTitle: undefined,
});
+7 -3
View File
@@ -34,6 +34,7 @@ type TaskSwitcherItem = {
remoteExecutorType?: string;
remoteExecutorName?: string;
updatedAt?: string;
createdAt?: string;
isArchived?: boolean;
primarySessionId?: string | null;
parentTaskTitle?: string;
@@ -206,10 +207,13 @@ export const TaskSwitcher = memo(function TaskSwitcher({
else backlog.push(task);
}
const byCreatedAt = (a: TaskSwitcherItem, b: TaskSwitcherItem) =>
(a.createdAt ?? "").localeCompare(b.createdAt ?? "");
return [
{ label: "Turn Finished", tasks: review },
{ label: "Running", tasks: inProgress },
{ label: "Backlog", tasks: backlog },
{ label: "Turn Finished", tasks: review.sort(byCreatedAt) },
{ label: "Running", tasks: inProgress.sort(byCreatedAt) },
{ label: "Backlog", tasks: backlog.sort(byCreatedAt) },
] satisfies Section[];
}, [tasks]);
@@ -29,6 +29,7 @@ function mapSnapshotTask(task: Task, stepIds: Set<string>): KanbanTask | null {
isRemoteExecutor: task.is_remote_executor ?? false,
parentTaskId: task.parent_id ?? undefined,
updatedAt: task.updated_at,
createdAt: task.created_at,
} as KanbanTask;
}
@@ -75,6 +75,7 @@ export function useKanbanData({
sessionCount: task.sessionCount,
reviewStatus: task.reviewStatus,
parentTaskId: task.parentTaskId,
createdAt: task.createdAt,
}));
const activeSteps = kanban.workflowId ? steps : [];
@@ -38,6 +38,7 @@ export type KanbanState = {
isRemoteExecutor?: boolean;
parentTaskId?: string | null;
updatedAt?: string;
createdAt?: string;
}>;
isLoading?: boolean;
};
+34 -10
View File
@@ -11,6 +11,39 @@ function withFallback<T>(value: T | null | undefined, fallback: T | undefined):
return value ?? fallback;
}
function buildNullableFields(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
payload: any,
existing?: KanbanTask,
): Pick<
KanbanTask,
| "repositoryId"
| "primarySessionId"
| "primarySessionState"
| "sessionCount"
| "reviewStatus"
| "primaryExecutorId"
| "primaryExecutorType"
| "primaryExecutorName"
| "parentTaskId"
| "updatedAt"
| "createdAt"
> {
return {
repositoryId: withFallback(payload.repository_id, existing?.repositoryId),
primarySessionId: withFallback(payload.primary_session_id, existing?.primarySessionId),
primarySessionState: withFallback(payload.primary_session_state, existing?.primarySessionState),
sessionCount: withFallback(payload.session_count, existing?.sessionCount),
reviewStatus: withFallback(payload.review_status, existing?.reviewStatus),
primaryExecutorId: withFallback(payload.primary_executor_id, existing?.primaryExecutorId),
primaryExecutorType: withFallback(payload.primary_executor_type, existing?.primaryExecutorType),
primaryExecutorName: withFallback(payload.primary_executor_name, existing?.primaryExecutorName),
parentTaskId: withFallback(payload.parent_id, existing?.parentTaskId),
updatedAt: withFallback(payload.updated_at, existing?.updatedAt),
createdAt: withFallback(payload.created_at, existing?.createdAt),
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function buildTaskFromPayload(payload: any, existing?: KanbanTask): KanbanTask {
return {
@@ -20,17 +53,8 @@ function buildTaskFromPayload(payload: any, existing?: KanbanTask): KanbanTask {
description: payload.description,
position: payload.position ?? 0,
state: payload.state,
repositoryId: withFallback(payload.repository_id, existing?.repositoryId),
primarySessionId: withFallback(payload.primary_session_id, existing?.primarySessionId),
primarySessionState: withFallback(payload.primary_session_state, existing?.primarySessionState),
sessionCount: withFallback(payload.session_count, existing?.sessionCount),
reviewStatus: withFallback(payload.review_status, existing?.reviewStatus),
primaryExecutorId: withFallback(payload.primary_executor_id, existing?.primaryExecutorId),
primaryExecutorType: withFallback(payload.primary_executor_type, existing?.primaryExecutorType),
primaryExecutorName: withFallback(payload.primary_executor_name, existing?.primaryExecutorName),
isRemoteExecutor: payload.is_remote_executor ?? existing?.isRemoteExecutor ?? false,
parentTaskId: withFallback(payload.parent_id, existing?.parentTaskId),
updatedAt: withFallback(payload.updated_at, existing?.updatedAt),
...buildNullableFields(payload, existing),
};
}