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