mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-09 16:02:36 +01:00
Save task sort choices in URLs across task views. Open routine tasks by creation time, newest first, with stable sorting before pagination. Co-Authored-By: Paperclip <Paperclip-Paperclip@users.noreply.github.com>
57 lines
2.9 KiB
TypeScript
57 lines
2.9 KiB
TypeScript
import type { Issue } from "@paperclipai/shared";
|
|
import { issuePriorityOrder, issueStatusOrder } from "./issue-filters";
|
|
import { workflowSort } from "./workflow-sort";
|
|
|
|
/** URL sort fields are shared by all task interfaces, including hidden columns. */
|
|
export const ISSUE_SORT_FIELDS = ["status", "priority", "title", "created", "updated", "workflow", "taskId", "createdBy", "assignee", "project", "parentTask", "tags", "worktree"] as const;
|
|
|
|
export type IssueSortField = typeof ISSUE_SORT_FIELDS[number];
|
|
export type IssueSort = { sortField: IssueSortField; sortDir: "asc" | "desc" };
|
|
|
|
export function readIssueSortParams(params: URLSearchParams): IssueSort | undefined {
|
|
const field = params.get("sortField");
|
|
if (!ISSUE_SORT_FIELDS.includes(field as IssueSortField)) return undefined;
|
|
return { sortField: field as IssueSortField, sortDir: params.get("sortDir") === "asc" ? "asc" : "desc" };
|
|
}
|
|
|
|
export type IssueSortContext = {
|
|
createdBy?: string | null;
|
|
assignee?: string | null;
|
|
project?: string | null;
|
|
parentTask?: string | null;
|
|
};
|
|
|
|
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
|
|
|
|
/** Compare the same raw values in every interface. Missing values stay last. */
|
|
export function sortIssues(issues: readonly Issue[], sort: IssueSort, context: (issue: Issue) => IssueSortContext = () => ({})): Issue[] {
|
|
if (sort.sortField === "workflow") {
|
|
const ordered = workflowSort([...issues]);
|
|
return sort.sortDir === "desc" ? ordered.reverse() : ordered;
|
|
}
|
|
const value = (issue: Issue): string | number | null | undefined => {
|
|
switch (sort.sortField) {
|
|
case "status": return issueStatusOrder.indexOf(issue.displayStatus ?? issue.status);
|
|
case "priority": return issuePriorityOrder.indexOf(issue.priority);
|
|
case "title": return issue.title;
|
|
case "created": return new Date(issue.createdAt).getTime();
|
|
case "updated": return new Date(issue.updatedAt).getTime();
|
|
case "taskId": return issue.identifier;
|
|
case "createdBy": return context(issue).createdBy;
|
|
case "assignee": return context(issue).assignee;
|
|
case "project": return context(issue).project ?? issue.project?.name;
|
|
case "parentTask": return context(issue).parentTask;
|
|
case "tags": return issue.labels?.map(label => label.name).sort().join(", ");
|
|
case "worktree": return issue.currentExecutionWorkspace?.branchName;
|
|
}
|
|
};
|
|
return [...issues].sort((a, b) => {
|
|
const left = value(a), right = value(b);
|
|
const missing = (value: string | number | null | undefined) => value == null || value === "" || (typeof value === "number" && !Number.isFinite(value));
|
|
const leftMissing = missing(left), rightMissing = missing(right);
|
|
if (leftMissing || rightMissing) return Number(leftMissing) - Number(rightMissing);
|
|
const result = typeof left === "number" && typeof right === "number" ? left - right : collator.compare(String(left), String(right));
|
|
return sort.sortDir === "asc" ? result : -result;
|
|
});
|
|
}
|