fix(activity): use API route metadata for feed links
This commit is contained in:
@@ -89,9 +89,11 @@ type FeedItem = {
|
||||
actor_role: string | null;
|
||||
board_id: string | null;
|
||||
board_name: string | null;
|
||||
board_href: string | null;
|
||||
task_id: string | null;
|
||||
task_title: string | null;
|
||||
title: string;
|
||||
context_href: string | null;
|
||||
};
|
||||
|
||||
type TaskMeta = {
|
||||
@@ -99,6 +101,10 @@ type TaskMeta = {
|
||||
boardId: string | null;
|
||||
};
|
||||
|
||||
type ActivityRouteParams = Record<string, string>;
|
||||
|
||||
const ACTIVITY_FEED_PATH = "/activity";
|
||||
|
||||
const TASK_EVENT_TYPES = new Set<TaskEventType>([
|
||||
"task.comment",
|
||||
"task.created",
|
||||
@@ -120,6 +126,73 @@ const formatShortTimestamp = (value: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
const normalizeRouteParams = (
|
||||
params: ActivityEventRead["route_params"] | ActivityRouteParams | null | undefined,
|
||||
): ActivityRouteParams => {
|
||||
if (!params || typeof params !== "object") return {};
|
||||
return Object.entries(params).reduce<ActivityRouteParams>((acc, [key, value]) => {
|
||||
if (typeof value === "string" && value.length > 0) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const buildRouteHref = (
|
||||
routeName: string | null | undefined,
|
||||
routeParams: ActivityRouteParams,
|
||||
fallback: {
|
||||
eventId: string;
|
||||
eventType: string;
|
||||
createdAt: string;
|
||||
taskId: string | null;
|
||||
},
|
||||
): string => {
|
||||
if (routeName === "board.approvals") {
|
||||
const boardId = routeParams.boardId;
|
||||
if (boardId) {
|
||||
return `/boards/${encodeURIComponent(boardId)}/approvals`;
|
||||
}
|
||||
}
|
||||
|
||||
if (routeName === "board") {
|
||||
const boardId = routeParams.boardId;
|
||||
if (boardId) {
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(routeParams).forEach(([key, value]) => {
|
||||
if (key !== "boardId") params.set(key, value);
|
||||
});
|
||||
const query = params.toString();
|
||||
return query
|
||||
? `/boards/${encodeURIComponent(boardId)}?${query}`
|
||||
: `/boards/${encodeURIComponent(boardId)}`;
|
||||
}
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(
|
||||
Object.keys(routeParams).length > 0
|
||||
? routeParams
|
||||
: {
|
||||
eventId: fallback.eventId,
|
||||
eventType: fallback.eventType,
|
||||
createdAt: fallback.createdAt,
|
||||
},
|
||||
);
|
||||
if (fallback.taskId && !params.has("taskId")) {
|
||||
params.set("taskId", fallback.taskId);
|
||||
}
|
||||
return `${ACTIVITY_FEED_PATH}?${params.toString()}`;
|
||||
};
|
||||
|
||||
const buildBoardHref = (
|
||||
routeParams: ActivityRouteParams,
|
||||
boardId: string | null,
|
||||
): string | null => {
|
||||
const resolved = routeParams.boardId ?? boardId;
|
||||
if (!resolved) return null;
|
||||
return `/boards/${encodeURIComponent(resolved)}`;
|
||||
};
|
||||
|
||||
const feedItemElementId = (id: string): string =>
|
||||
`activity-item-${id.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
|
||||
|
||||
@@ -219,11 +292,6 @@ const FeedCard = memo(function FeedCard({
|
||||
}) {
|
||||
const message = (item.message ?? "").trim();
|
||||
const authorAvatar = (item.actor_name[0] ?? "A").toUpperCase();
|
||||
const taskHref =
|
||||
item.board_id && item.task_id
|
||||
? `/boards/${item.board_id}?taskId=${item.task_id}`
|
||||
: null;
|
||||
const boardHref = item.board_id ? `/boards/${item.board_id}` : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -241,9 +309,9 @@ const FeedCard = memo(function FeedCard({
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="min-w-0">
|
||||
{taskHref ? (
|
||||
{item.context_href ? (
|
||||
<Link
|
||||
href={taskHref}
|
||||
href={item.context_href}
|
||||
className="block text-sm font-semibold leading-snug text-slate-900 transition hover:text-slate-950 hover:underline"
|
||||
title={item.title}
|
||||
style={{
|
||||
@@ -269,9 +337,9 @@ const FeedCard = memo(function FeedCard({
|
||||
>
|
||||
{eventLabel(item.event_type)}
|
||||
</span>
|
||||
{boardHref && item.board_name ? (
|
||||
{item.board_href && item.board_name ? (
|
||||
<Link
|
||||
href={boardHref}
|
||||
href={item.board_href}
|
||||
className="font-semibold text-slate-700 hover:text-slate-900 hover:underline"
|
||||
>
|
||||
{item.board_name}
|
||||
@@ -424,12 +492,30 @@ export default function ActivityPage() {
|
||||
);
|
||||
|
||||
const mapTaskActivity = useCallback(
|
||||
(event: ActivityEventRead): FeedItem | null => {
|
||||
(
|
||||
event: ActivityEventRead,
|
||||
fallbackBoardId: string | null = null,
|
||||
): FeedItem | null => {
|
||||
if (!isTaskEventType(event.event_type)) return null;
|
||||
const meta = event.task_id
|
||||
? taskMetaByIdRef.current.get(event.task_id)
|
||||
: null;
|
||||
const boardId = meta?.boardId ?? null;
|
||||
const routeName = event.route_name ?? null;
|
||||
const routeParams = normalizeRouteParams(event.route_params);
|
||||
const taskId = event.task_id ?? routeParams.taskId ?? null;
|
||||
const boardId =
|
||||
meta?.boardId ??
|
||||
event.board_id ??
|
||||
routeParams.boardId ??
|
||||
fallbackBoardId ??
|
||||
null;
|
||||
const fallbackRouteParams: ActivityRouteParams = {};
|
||||
if (boardId) fallbackRouteParams.boardId = boardId;
|
||||
if (taskId) fallbackRouteParams.taskId = taskId;
|
||||
const effectiveRouteParams =
|
||||
Object.keys(routeParams).length > 0 ? routeParams : fallbackRouteParams;
|
||||
const effectiveRouteName =
|
||||
routeName ?? (boardId ? "board" : "activity");
|
||||
const author = resolveAuthor(event.agent_id, currentUserDisplayName);
|
||||
return {
|
||||
id: `activity:${event.id}`,
|
||||
@@ -442,10 +528,17 @@ export default function ActivityPage() {
|
||||
actor_role: author.role,
|
||||
board_id: boardId,
|
||||
board_name: boardNameForId(boardId),
|
||||
task_id: event.task_id ?? null,
|
||||
board_href: buildBoardHref(effectiveRouteParams, boardId),
|
||||
task_id: taskId,
|
||||
task_title: meta?.title ?? null,
|
||||
title:
|
||||
meta?.title ?? (event.task_id ? "Unknown task" : "Task activity"),
|
||||
meta?.title ?? (taskId ? "Unknown task" : "Task activity"),
|
||||
context_href: buildRouteHref(effectiveRouteName, effectiveRouteParams, {
|
||||
eventId: event.id,
|
||||
eventType: event.event_type,
|
||||
createdAt: event.created_at,
|
||||
taskId,
|
||||
}),
|
||||
};
|
||||
},
|
||||
[boardNameForId, currentUserDisplayName, resolveAuthor],
|
||||
@@ -457,6 +550,11 @@ export default function ActivityPage() {
|
||||
? taskMetaByIdRef.current.get(comment.task_id)
|
||||
: null;
|
||||
const boardId = meta?.boardId ?? fallbackBoardId;
|
||||
const taskId = comment.task_id ?? null;
|
||||
const routeParams: ActivityRouteParams = {};
|
||||
if (boardId) routeParams.boardId = boardId;
|
||||
if (taskId) routeParams.taskId = taskId;
|
||||
routeParams.commentId = comment.id;
|
||||
const author = resolveAuthor(comment.agent_id, currentUserDisplayName);
|
||||
return {
|
||||
id: `comment:${comment.id}`,
|
||||
@@ -469,10 +567,17 @@ export default function ActivityPage() {
|
||||
actor_role: author.role,
|
||||
board_id: boardId,
|
||||
board_name: boardNameForId(boardId),
|
||||
task_id: comment.task_id ?? null,
|
||||
board_href: buildBoardHref(routeParams, boardId),
|
||||
task_id: taskId,
|
||||
task_title: meta?.title ?? null,
|
||||
title:
|
||||
meta?.title ?? (comment.task_id ? "Unknown task" : "Task activity"),
|
||||
meta?.title ?? (taskId ? "Unknown task" : "Task activity"),
|
||||
context_href: buildRouteHref("board", routeParams, {
|
||||
eventId: comment.id,
|
||||
eventType: "task.comment",
|
||||
createdAt: comment.created_at,
|
||||
taskId,
|
||||
}),
|
||||
};
|
||||
},
|
||||
[boardNameForId, currentUserDisplayName, resolveAuthor],
|
||||
@@ -525,6 +630,8 @@ export default function ActivityPage() {
|
||||
const taskMeta = approval.task_id
|
||||
? taskMetaByIdRef.current.get(approval.task_id)
|
||||
: null;
|
||||
const routeParams: ActivityRouteParams = { boardId };
|
||||
const taskId = approval.task_id ?? null;
|
||||
|
||||
return {
|
||||
id: `approval:${approval.id}:${kind}:${stamp}`,
|
||||
@@ -537,9 +644,16 @@ export default function ActivityPage() {
|
||||
actor_role: author.role,
|
||||
board_id: boardId,
|
||||
board_name: boardNameForId(boardId),
|
||||
task_id: approval.task_id ?? null,
|
||||
board_href: buildBoardHref(routeParams, boardId),
|
||||
task_id: taskId,
|
||||
task_title: taskMeta?.title ?? null,
|
||||
title: `Approval · ${action}`,
|
||||
context_href: buildRouteHref("board.approvals", routeParams, {
|
||||
eventId: approval.id,
|
||||
eventType: kind,
|
||||
createdAt: stamp,
|
||||
taskId,
|
||||
}),
|
||||
};
|
||||
},
|
||||
[boardNameForId, currentUserDisplayName, resolveAuthor],
|
||||
@@ -553,6 +667,7 @@ export default function ActivityPage() {
|
||||
currentUserDisplayName,
|
||||
);
|
||||
const command = content.startsWith("/");
|
||||
const routeParams: ActivityRouteParams = { boardId, panel: "chat" };
|
||||
return {
|
||||
id: `chat:${memory.id}`,
|
||||
created_at: memory.created_at,
|
||||
@@ -564,9 +679,16 @@ export default function ActivityPage() {
|
||||
actor_role: null,
|
||||
board_id: boardId,
|
||||
board_name: boardNameForId(boardId),
|
||||
board_href: buildBoardHref(routeParams, boardId),
|
||||
task_id: null,
|
||||
task_title: null,
|
||||
title: command ? "Board command" : "Board chat",
|
||||
context_href: buildRouteHref("board", routeParams, {
|
||||
eventId: memory.id,
|
||||
eventType: command ? "board.command" : "board.chat",
|
||||
createdAt: memory.created_at,
|
||||
taskId: null,
|
||||
}),
|
||||
};
|
||||
},
|
||||
[boardNameForId, currentUserDisplayName],
|
||||
@@ -618,6 +740,10 @@ export default function ActivityPage() {
|
||||
: kind === "agent.offline"
|
||||
? `${agent.name} is offline.`
|
||||
: `${agent.name} updated (${humanizeStatus(nextStatus)}).`;
|
||||
const boardId = agent.board_id ?? null;
|
||||
const routeParams: ActivityRouteParams = boardId
|
||||
? { boardId }
|
||||
: {};
|
||||
|
||||
return {
|
||||
id: `agent:${agent.id}:${isSnapshot ? "snapshot" : kind}:${stamp}`,
|
||||
@@ -628,11 +754,21 @@ export default function ActivityPage() {
|
||||
agent_id: agent.id,
|
||||
actor_name: agent.name,
|
||||
actor_role: roleFromAgent(agent),
|
||||
board_id: agent.board_id ?? null,
|
||||
board_name: boardNameForId(agent.board_id),
|
||||
board_id: boardId,
|
||||
board_name: boardNameForId(boardId),
|
||||
board_href: buildBoardHref(routeParams, boardId),
|
||||
task_id: null,
|
||||
task_title: null,
|
||||
title: `Agent · ${agent.name}`,
|
||||
context_href:
|
||||
boardId === null
|
||||
? null
|
||||
: buildRouteHref("board", routeParams, {
|
||||
eventId: agent.id,
|
||||
eventType: kind,
|
||||
createdAt: stamp,
|
||||
taskId: null,
|
||||
}),
|
||||
};
|
||||
},
|
||||
[boardNameForId],
|
||||
@@ -871,12 +1007,8 @@ export default function ActivityPage() {
|
||||
updateTaskMeta(payload.task, boardId);
|
||||
}
|
||||
if (payload.activity) {
|
||||
const mapped = mapTaskActivity(payload.activity);
|
||||
const mapped = mapTaskActivity(payload.activity, boardId);
|
||||
if (mapped) {
|
||||
if (!mapped.board_id) {
|
||||
mapped.board_id = boardId;
|
||||
mapped.board_name = boardNameForId(boardId);
|
||||
}
|
||||
if (!mapped.task_title && payload.task?.title) {
|
||||
mapped.task_title = payload.task.title;
|
||||
mapped.title = payload.task.title;
|
||||
|
||||
Reference in New Issue
Block a user