feat(agent-api): PATCH /api/agent/cards/:cardId/move
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ded7ffbb1f
commit
19705cc8d9
1 changed files with 103 additions and 0 deletions
103
src/app/api/agent/cards/[cardId]/move/route.ts
Normal file
103
src/app/api/agent/cards/[cardId]/move/route.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
import { getAdminPb } from "@/lib/pb/admin";
|
||||||
|
import { requireAgentToken } from "@/lib/agent/auth";
|
||||||
|
import { getOwnerUser } from "@/lib/agent/owner";
|
||||||
|
import { logActivity } from "@/app/actions/activity";
|
||||||
|
import { relinkProject } from "@/lib/linking/engine";
|
||||||
|
import { positionBetween } from "@/lib/ordering/fractional";
|
||||||
|
import { jsonError, jsonOk } from "@/lib/http/json";
|
||||||
|
import type { CardRecord, ColumnRecord } from "@/lib/pb/types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH /api/agent/cards/:cardId/move — move a card by destination column
|
||||||
|
* NAME (not id — the agent has no reason to know internal PocketBase ids),
|
||||||
|
* resolved case-insensitively against the card's own project.
|
||||||
|
*
|
||||||
|
* Always appends to the end of the destination column — there is no concept
|
||||||
|
* of "drop position" for a machine caller, so it lands last, exactly like a
|
||||||
|
* freshly created card does.
|
||||||
|
*/
|
||||||
|
export async function PATCH(
|
||||||
|
request: Request,
|
||||||
|
context: { params: Promise<{ cardId: string }> }
|
||||||
|
): Promise<Response> {
|
||||||
|
const denied = requireAgentToken(request);
|
||||||
|
if (denied) return denied;
|
||||||
|
|
||||||
|
const { cardId } = await context.params;
|
||||||
|
|
||||||
|
let json: unknown;
|
||||||
|
try {
|
||||||
|
json = await request.json();
|
||||||
|
} catch {
|
||||||
|
return jsonError("Invalid request body", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnName = (json as { column?: unknown } | null)?.column;
|
||||||
|
if (typeof columnName !== "string" || !columnName.trim()) {
|
||||||
|
return jsonError("A destination column name is required", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pb = await getAdminPb();
|
||||||
|
const card = await pb.collection("cards").getOne<CardRecord>(cardId).catch(() => null);
|
||||||
|
if (!card || card.archived) return jsonError("Card not found", 404);
|
||||||
|
|
||||||
|
const columns = await pb.collection("columns").getFullList<ColumnRecord>({
|
||||||
|
filter: `project = "${card.project}"`,
|
||||||
|
sort: "position",
|
||||||
|
});
|
||||||
|
|
||||||
|
const destination = columns.find(
|
||||||
|
(c) => c.name.toLowerCase() === columnName.trim().toLowerCase()
|
||||||
|
);
|
||||||
|
if (!destination) {
|
||||||
|
return jsonError(
|
||||||
|
`Unknown column "${columnName}". This project's columns are: ${columns
|
||||||
|
.map((c) => c.name)
|
||||||
|
.join(", ")}`,
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastInDestination = await pb
|
||||||
|
.collection("cards")
|
||||||
|
.getFirstListItem<CardRecord>(`column = "${destination.id}" && archived = false`, {
|
||||||
|
sort: "-position",
|
||||||
|
})
|
||||||
|
.catch(() => null);
|
||||||
|
const position = positionBetween(lastInDestination ? lastInDestination.position : null, null);
|
||||||
|
|
||||||
|
// Same accept-transition rule as the owner's own drag-and-drop move: a
|
||||||
|
// card leaving the Inbox for a real working column, for the first time,
|
||||||
|
// is being accepted — not just relocated.
|
||||||
|
const accepting = !card.accepted && card.column !== destination.id && !destination.is_inbox;
|
||||||
|
|
||||||
|
const updated = await pb.collection("cards").update<CardRecord>(cardId, {
|
||||||
|
column: destination.id,
|
||||||
|
position,
|
||||||
|
...(accepting
|
||||||
|
? { accepted: true, start_date: card.start_date || new Date().toISOString().slice(0, 10) }
|
||||||
|
: {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (accepting) {
|
||||||
|
const owner = await getOwnerUser();
|
||||||
|
await logActivity({
|
||||||
|
project: card.project,
|
||||||
|
card: card.id,
|
||||||
|
actor: owner.id,
|
||||||
|
action: "card_accepted",
|
||||||
|
newValue: destination.name,
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("[agent-api] accepted card but activity failed", error);
|
||||||
|
});
|
||||||
|
await relinkProject(card.project).catch((error) => {
|
||||||
|
console.error("[agent-api] accepted card but relinking failed", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonOk({
|
||||||
|
id: updated.id,
|
||||||
|
column: destination.name,
|
||||||
|
accepted: updated.accepted,
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue