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