mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-07 07:55:47 +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>
86 lines
4.2 KiB
TypeScript
86 lines
4.2 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import request from "supertest";
|
|
import { expect, it } from "vitest";
|
|
import { issues } from "@paperclipai/db";
|
|
import { issueRoutes } from "../routes/issues.js";
|
|
import {
|
|
describeEmbeddedPostgres,
|
|
resetCompanyIssueFixtures,
|
|
routeApp,
|
|
seedCompanyWithBoardAccess,
|
|
useEmbeddedPostgres,
|
|
} from "./helpers/route-test-harness.js";
|
|
|
|
/**
|
|
* Regression coverage for https://github.com/paperclipai/paperclip/issues/4628.
|
|
* Express's `qs` parser hands the list route either a string or an array for
|
|
* `?status=`, and the route normalizes both shapes.
|
|
*/
|
|
|
|
describeEmbeddedPostgres("issue list status query parsing", () => {
|
|
const ctx = useEmbeddedPostgres("paperclip-issues-list-query-parsing-", {
|
|
resetEach: resetCompanyIssueFixtures,
|
|
});
|
|
|
|
async function listStatuses(query: string) {
|
|
const company = await seedCompanyWithBoardAccess(ctx.db, "Status parsing");
|
|
const companyId = company.companyId;
|
|
await ctx.db.insert(issues).values([
|
|
{ id: randomUUID(), companyId, title: "Todo", status: "todo", priority: "medium" },
|
|
{ id: randomUUID(), companyId, title: "In progress", status: "in_progress", priority: "medium" },
|
|
{ id: randomUUID(), companyId, title: "Done", status: "done", priority: "medium" },
|
|
]);
|
|
const res = await request(routeApp(ctx.db, company.actor, issueRoutes))
|
|
.get(`/api/companies/${companyId}/issues${query}`)
|
|
.expect(200);
|
|
return (res.body as { status: string }[]).map((issue) => issue.status).sort();
|
|
}
|
|
|
|
it("accepts a single ?status=todo", async () => {
|
|
expect(await listStatuses("?status=todo")).toEqual(["todo"]);
|
|
});
|
|
|
|
it("accepts comma-separated ?status=todo,in_progress", async () => {
|
|
expect(await listStatuses("?status=todo,in_progress")).toEqual(["in_progress", "todo"]);
|
|
});
|
|
|
|
it("accepts repeated ?status=todo&status=in_progress", async () => {
|
|
expect(await listStatuses("?status=todo&status=in_progress")).toEqual(["in_progress", "todo"]);
|
|
});
|
|
|
|
it("accepts mixed array and CSV ?status=todo,in_progress&status=done", async () => {
|
|
expect(await listStatuses("?status=todo,in_progress&status=done"))
|
|
.toEqual(["done", "in_progress", "todo"]);
|
|
});
|
|
|
|
it("returns every status when ?status is absent", async () => {
|
|
expect(await listStatuses("")).toEqual(["done", "in_progress", "todo"]);
|
|
});
|
|
it.each(["", "&view=compact", "&view=compact&projection=index"])("rejects unsupported creation sorting for blocked attention (%s)", async projection => {
|
|
const { companyId, actor } = await seedCompanyWithBoardAccess(ctx.db, "Blocked sorting");
|
|
await request(routeApp(ctx.db, actor, issueRoutes))
|
|
.get(`/api/companies/${companyId}/issues?attention=blocked&sortField=created&sortDir=asc${projection}`)
|
|
.expect(422);
|
|
});
|
|
|
|
it.each(["", "&view=compact", "&view=compact&projection=index"])("sorts routine tasks by creation time before pagination (%s)", async (projection) => {
|
|
const { companyId, actor } = await seedCompanyWithBoardAccess(ctx.db, "Creation sorting");
|
|
const ids = Array.from({ length: 4 }, (_, index) => `00000000-0000-4000-8000-00000000000${index}`);
|
|
await ctx.db.insert(issues).values([
|
|
...ids.map((id, index) => ({ id, companyId, title: "Routine sorting", originKind: "routine_execution", status: "todo",
|
|
createdAt: new Date(index < 2 ? "2026-01-01" : "2026-02-01"),
|
|
updatedAt: new Date(index < 2 ? "2026-03-01" : "2026-02-01"), priority: index < 2 ? "critical" : "low" })),
|
|
{ companyId, title: "Routine sorting manual", originKind: "manual", createdAt: new Date("2026-04-01") },
|
|
]);
|
|
const app = routeApp(ctx.db, actor, issueRoutes);
|
|
const path = `/api/companies/${companyId}/issues?originKind=routine_execution&includeRoutineExecutions=true&sortField=created&limit=2${projection}`;
|
|
for (const sortDir of ["asc", "desc"]) {
|
|
const first = await request(app).get(`${path}&sortDir=${sortDir}`).expect(200);
|
|
const second = await request(app).get(`${path}&sortDir=${sortDir}&offset=2`).expect(200);
|
|
expect([...first.body, ...second.body].map((issue: { id: string }) => issue.id)).toEqual(sortDir === "asc" ? ids : [...ids].reverse());
|
|
}
|
|
await request(app).get(`${path}&sortDir=invalid`).expect(400);
|
|
});
|
|
|
|
});
|